diff --git a/.gitignore b/.gitignore index 9114bb7b..a92b5f8b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__/* static_files/* static/logo/* doc/build/ +media/* diff --git a/README.md b/README.md index fb234ea1..a99a76f2 100644 --- a/README.md +++ b/README.md @@ -171,10 +171,9 @@ re2o/wsgi.py permet de fonctionner avec apache2 en production Une fois démaré, le site web devrait être accessible. Pour créer un premier user, faire '''python3 manage.py createsuperuser''' qui va alors créer un user admin. -Il est conseillé de créer alors les droits cableur, bureau, trésorier et infra, -qui n'existent pas par défaut dans le menu adhérents. -Il est également conseillé de créer un user portant le nom de -l'association/organisation, qui possedera l'ensemble des machines. +Il est conseillé de créer un user portant le nom de +l'association/organisation, qui possedera l'ensemble des machines, à indiquer +dans le menu reglages sur l'interface. ## Installations Optionnelles ### Générer le schéma des dépendances diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 00000000..1b02c577 --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,360 @@ +# 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 © 2018 Mael Kervella +# +# 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. + +""" +Serializers for the API app +""" + +from rest_framework import serializers +from users.models import Club, Adherent +from machines.models import ( + Interface, + IpType, + Extension, + IpList, + MachineType, + Domain, + Txt, + Mx, + Srv, + Service_link, + Ns, + OuverturePortList, + OuverturePort, + Ipv6List +) + + +class ServiceLinkSerializer(serializers.ModelSerializer): + name = serializers.CharField(source='service.service_type') + + class Meta: + model = Service_link + fields = ('name',) + + +class MailingSerializer(serializers.ModelSerializer): + name = serializers.CharField(source='pseudo') + + class Meta: + model = Club + fields = ('name',) + + +class MailingMemberSerializer(serializers.ModelSerializer): + class Meta: + model = Adherent + fields = ('email', 'name', 'surname', 'pseudo',) + + +class IpTypeField(serializers.RelatedField): + """Serialisation d'une iptype, renvoie son evaluation str""" + def to_representation(self, value): + return value.type + + +class IpListSerializer(serializers.ModelSerializer): + """Serialisation d'une iplist, ip_type etant une foreign_key, + on evalue sa methode str""" + ip_type = IpTypeField(read_only=True) + + class Meta: + model = IpList + fields = ('ipv4', 'ip_type') + + +class Ipv6ListSerializer(serializers.ModelSerializer): + class Meta: + model = Ipv6List + fields = ('ipv6', 'slaac_ip') + + +class InterfaceSerializer(serializers.ModelSerializer): + """Serialisation d'une interface, ipv4, domain et extension sont + des foreign_key, on les override et on les evalue avec des fonctions + get_...""" + ipv4 = IpListSerializer(read_only=True) + mac_address = serializers.SerializerMethodField('get_macaddress') + domain = serializers.SerializerMethodField('get_dns') + extension = serializers.SerializerMethodField('get_interface_extension') + + class Meta: + model = Interface + fields = ('ipv4', 'mac_address', 'domain', 'extension') + + def get_dns(self, obj): + return obj.domain.name + + def get_interface_extension(self, obj): + return obj.domain.extension.name + + def get_macaddress(self, obj): + return str(obj.mac_address) + + +class FullInterfaceSerializer(serializers.ModelSerializer): + """Serialisation complete d'une interface avec les ipv6 en plus""" + ipv4 = IpListSerializer(read_only=True) + ipv6 = Ipv6ListSerializer(read_only=True, many=True) + mac_address = serializers.SerializerMethodField('get_macaddress') + domain = serializers.SerializerMethodField('get_dns') + extension = serializers.SerializerMethodField('get_interface_extension') + + class Meta: + model = Interface + fields = ('ipv4', 'ipv6', 'mac_address', 'domain', 'extension') + + def get_dns(self, obj): + return obj.domain.name + + def get_interface_extension(self, obj): + return obj.domain.extension.name + + def get_macaddress(self, obj): + return str(obj.mac_address) + + +class ExtensionNameField(serializers.RelatedField): + """Evaluation str d'un objet extension (.example.org)""" + def to_representation(self, value): + return value.name + + +class TypeSerializer(serializers.ModelSerializer): + """Serialisation d'un iptype : extension et la liste des + ouvertures de port son evalués en get_... etant des + foreign_key ou des relations manytomany""" + extension = ExtensionNameField(read_only=True) + ouverture_ports_tcp_in = serializers\ + .SerializerMethodField('get_port_policy_input_tcp') + ouverture_ports_tcp_out = serializers\ + .SerializerMethodField('get_port_policy_output_tcp') + ouverture_ports_udp_in = serializers\ + .SerializerMethodField('get_port_policy_input_udp') + ouverture_ports_udp_out = serializers\ + .SerializerMethodField('get_port_policy_output_udp') + + class Meta: + model = IpType + fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop', + 'prefix_v6', + 'ouverture_ports_tcp_in', 'ouverture_ports_tcp_out', + 'ouverture_ports_udp_in', 'ouverture_ports_udp_out',) + + def get_port_policy(self, obj, protocole, io): + if obj.ouverture_ports is None: + return [] + return map( + str, + obj.ouverture_ports.ouvertureport_set.filter( + protocole=protocole + ).filter(io=io) + ) + + def get_port_policy_input_tcp(self, obj): + """Renvoie la liste des ports ouverts en entrée tcp""" + return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.IN) + + def get_port_policy_output_tcp(self, obj): + """Renvoie la liste des ports ouverts en sortie tcp""" + return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.OUT) + + def get_port_policy_input_udp(self, obj): + """Renvoie la liste des ports ouverts en entrée udp""" + return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.IN) + + def get_port_policy_output_udp(self, obj): + """Renvoie la liste des ports ouverts en sortie udp""" + return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.OUT) + + +class ExtensionSerializer(serializers.ModelSerializer): + """Serialisation d'une extension : origin_ip et la zone sont + des foreign_key donc evalués en get_...""" + origin = serializers.SerializerMethodField('get_origin_ip') + zone_entry = serializers.SerializerMethodField('get_zone_name') + soa = serializers.SerializerMethodField('get_soa_data') + + class Meta: + model = Extension + fields = ('name', 'origin', 'origin_v6', 'zone_entry', 'soa') + + def get_origin_ip(self, obj): + return obj.origin.ipv4 + + def get_zone_name(self, obj): + return str(obj.dns_entry) + + def get_soa_data(self, obj): + return { 'mail': obj.soa.dns_soa_mail, 'param': obj.soa.dns_soa_param } + + +class MxSerializer(serializers.ModelSerializer): + """Serialisation d'un MX, evaluation du nom, de la zone + et du serveur cible, etant des foreign_key""" + name = serializers.SerializerMethodField('get_entry_name') + zone = serializers.SerializerMethodField('get_zone_name') + mx_entry = serializers.SerializerMethodField('get_mx_name') + + class Meta: + model = Mx + fields = ('zone', 'priority', 'name', 'mx_entry') + + def get_entry_name(self, obj): + return str(obj.name) + + def get_zone_name(self, obj): + return obj.zone.name + + def get_mx_name(self, obj): + return str(obj.dns_entry) + + +class TxtSerializer(serializers.ModelSerializer): + """Serialisation d'un txt : zone cible et l'entrée txt + sont evaluées à part""" + zone = serializers.SerializerMethodField('get_zone_name') + txt_entry = serializers.SerializerMethodField('get_txt_name') + + class Meta: + model = Txt + fields = ('zone', 'txt_entry', 'field1', 'field2') + + def get_zone_name(self, obj): + return str(obj.zone.name) + + def get_txt_name(self, obj): + return str(obj.dns_entry) + + +class SrvSerializer(serializers.ModelSerializer): + """Serialisation d'un srv : zone cible et l'entrée txt""" + extension = serializers.SerializerMethodField('get_extension_name') + srv_entry = serializers.SerializerMethodField('get_srv_name') + + class Meta: + model = Srv + fields = ( + 'service', + 'protocole', + 'extension', + 'ttl', + 'priority', + 'weight', + 'port', + 'target', + 'srv_entry' + ) + + def get_extension_name(self, obj): + return str(obj.extension.name) + + def get_srv_name(self, obj): + return str(obj.dns_entry) + + +class NsSerializer(serializers.ModelSerializer): + """Serialisation d'un NS : la zone, l'entrée ns complète et le serveur + ns sont évalués à part""" + zone = serializers.SerializerMethodField('get_zone_name') + ns = serializers.SerializerMethodField('get_domain_name') + ns_entry = serializers.SerializerMethodField('get_text_name') + + class Meta: + model = Ns + fields = ('zone', 'ns', 'ns_entry') + + def get_zone_name(self, obj): + return obj.zone.name + + def get_domain_name(self, obj): + return str(obj.ns) + + def get_text_name(self, obj): + return str(obj.dns_entry) + + +class DomainSerializer(serializers.ModelSerializer): + """Serialisation d'un domain, extension, cname sont des foreign_key, + et l'entrée complète, sont évalués à part""" + extension = serializers.SerializerMethodField('get_zone_name') + cname = serializers.SerializerMethodField('get_alias_name') + cname_entry = serializers.SerializerMethodField('get_cname_name') + + class Meta: + model = Domain + fields = ('name', 'extension', 'cname', 'cname_entry') + + def get_zone_name(self, obj): + return obj.extension.name + + def get_alias_name(self, obj): + return str(obj.cname) + + def get_cname_name(self, obj): + return str(obj.dns_entry) + + +class ServicesSerializer(serializers.ModelSerializer): + """Evaluation d'un Service, et serialisation""" + server = serializers.SerializerMethodField('get_server_name') + service = serializers.SerializerMethodField('get_service_name') + need_regen = serializers.SerializerMethodField('get_regen_status') + + class Meta: + model = Service_link + fields = ('server', 'service', 'need_regen') + + def get_server_name(self, obj): + return str(obj.server.domain.name) + + def get_service_name(self, obj): + return str(obj.service) + + def get_regen_status(self, obj): + return obj.need_regen() + + +class OuverturePortsSerializer(serializers.Serializer): + """Serialisation de l'ouverture des ports""" + ipv4 = serializers.SerializerMethodField() + ipv6 = serializers.SerializerMethodField() + + def get_ipv4(): + return {i.ipv4.ipv4: + { + "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], + "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + } + for i in Interface.objects.all() if i.ipv4 + } + + def get_ipv6(): + return {i.ipv6: + { + "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()], + "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()], + "udp_in":[j.udp_ports_in() for j in i.port_lists.all()], + "udp_out":[j.udp_ports_out() for j in i.port_lists.all()], + } + for i in Interface.objects.all() if i.ipv6 + } diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 00000000..21fa6d24 --- /dev/null +++ b/api/tests.py @@ -0,0 +1,25 @@ +# 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. + +from django.test import TestCase + +# Create your tests here. diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 00000000..584dd600 --- /dev/null +++ b/api/urls.py @@ -0,0 +1,59 @@ +# 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 © 2018 Mael Kervella +# +# 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. +"""api.urls + +Urls de l'api, pointant vers les fonctions de views +""" + +from __future__ import unicode_literals + +from django.conf.urls import url + +from . import views + + +urlpatterns = [ + # Services + url(r'^services/$', views.services), + url(r'^services/(?P\w+)/(?P\w+)/regen/$', views.services_server_service_regen), + url(r'^services/(?P\w+)/$', views.services_server), + + # DNS + url(r'^dns/mac-ip-dns/$', views.dns_mac_ip_dns), + url(r'^dns/alias/$', views.dns_alias), + url(r'^dns/corresp/$', views.dns_corresp), + url(r'^dns/mx/$', views.dns_mx), + url(r'^dns/ns/$', views.dns_ns), + url(r'^dns/txt/$', views.dns_txt), + url(r'^dns/srv/$', views.dns_srv), + url(r'^dns/zones/$', views.dns_zones), + + # Firewall + url(r'^firewall/ouverture_ports/$', views.firewall_ouverture_ports), + + # DHCP + url(r'^dhcp/mac-ip/$', views.dhcp_mac_ip), + + # Mailings + url(r'^mailing/standard/$', views.mailing_standard), + url(r'^mailing/standard/(?P\w+)/members/$', views.mailing_standard_ml_members), + url(r'^mailing/club/$', views.mailing_club), + url(r'^mailing/club/(?P\w+)/members/$', views.mailing_club_ml_members), +] diff --git a/api/utils.py b/api/utils.py new file mode 100644 index 00000000..024c9a39 --- /dev/null +++ b/api/utils.py @@ -0,0 +1,114 @@ +# 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 © 2018 Maël Kervella +# +# 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. + +"""api.utils. + +Set of various and usefull functions for the API app +""" + +from rest_framework.renderers import JSONRenderer +from django.http import HttpResponse + +class JSONResponse(HttpResponse): + """A JSON response that can be send as an HTTP response. + Usefull in case of REST API. + """ + + def __init__(self, data, **kwargs): + """Initialisz a JSONResponse object. + + Args: + data: the data to render as JSON (often made of lists, dicts, + strings, boolean and numbers). See `JSONRenderer.render(data)` for + further details. + + Creates: + An HTTPResponse containing the data in JSON format. + """ + + content = JSONRenderer().render(data) + kwargs['content_type'] = 'application/json' + super(JSONResponse, self).__init__(content, **kwargs) + + +class JSONError(JSONResponse): + """A JSON response when the request failed. + """ + + def __init__(self, error_msg, data=None, **kwargs): + """Initialise a JSONError object. + + Args: + error_msg: A message explaining where the error is. + data: An optional field for further data to send along. + + Creates: + A JSONResponse containing a field `status` set to `error` and a field + `reason` containing `error_msg`. If `data` argument has been given, + a field `data` containing it is added to the JSON response. + """ + + response = { + 'status' : 'error', + 'reason' : error_msg + } + if data is not None: + response['data'] = data + super(JSONError, self).__init__(response, **kwargs) + + +class JSONSuccess(JSONResponse): + """A JSON response when the request suceeded. + """ + + def __init__(self, data=None, **kwargs): + """Initialise a JSONSucess object. + + Args: + error_msg: A message explaining where the error is. + data: An optional field for further data to send along. + + Creates: + A JSONResponse containing a field `status` set to `sucess`. If `data` + argument has been given, a field `data` containing it is added to the + JSON response. + """ + + response = { + 'status' : 'success', + } + if data is not None: + response['data'] = data + super(JSONSuccess, self).__init__(response, **kwargs) + + +def accept_method(methods): + """Decorator to set a list of accepted request method. + Check if the method used is accepted. If not, send a NotAllowed response. + """ + def decorator(view): + def wrapper(request, *args, **kwargs): + if request.method in methods: + return view(request, *args, **kwargs) + else: + return JSONError('Invalid request method. Request methods authorize are '+str(methods)) + return view(request, *args, **kwargs) + return wrapper + return decorator diff --git a/api/views.py b/api/views.py new file mode 100644 index 00000000..cc2237de --- /dev/null +++ b/api/views.py @@ -0,0 +1,464 @@ +# 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 © 2018 Maël Kervella +# +# 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. + +"""api.views + +The views for the API app. They should all return JSON data and not fallback on +HTML pages such as the login and index pages for a better integration. +""" + +from django.contrib.auth.decorators import login_required, permission_required +from django.views.decorators.csrf import csrf_exempt + +from re2o.utils import all_has_access, all_active_assigned_interfaces + +from users.models import Club +from machines.models import (Service_link, Service, Interface, Domain, + OuverturePortList) + +from .serializers import * +from .utils import JSONError, JSONSuccess, accept_method + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def services(request): + """The list of the different services and servers couples + + Return: + GET: + A JSONSuccess response with a field `data` containing: + * a list of dictionnaries (one for each service-server couple) containing: + * a field `server`: the server name + * a field `service`: the service name + * a field `need_regen`: does the service need a regeneration ? + """ + service_link = Service_link.objects.all().select_related('server__domain').select_related('service') + seria = ServicesSerializer(service_link, many=True) + return JSONSuccess(seria.data) + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET', 'POST']) +def services_server_service_regen(request, server_name, service_name): + """The status of a particular service linked to a particular server. + Mark the service as regenerated if POST used. + + Returns: + GET: + A JSONSucess response with a field `data` containing: + * a field `need_regen`: does the service need a regeneration ? + + POST: + An empty JSONSuccess response. + """ + query = Service_link.objects.filter( + service__in=Service.objects.filter(service_type=service_name), + server__in=Interface.objects.filter( + domain__in=Domain.objects.filter(name=server_name) + ) + ) + if not query: + return JSONError("This service is not active for this server") + + service = query.first() + if request.method == 'GET': + return JSONSuccess({'need_regen': service.need_regen()}) + else: + service.done_regen() + return JSONSuccess() + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def services_server(request, server_name): + """The list of services attached to a specific server + + Returns: + GET: + A JSONSuccess response with a field `data` containing: + * a list of dictionnaries (one for each service) containing: + * a field `name`: the name of a service + """ + query = Service_link.objects.filter( + server__in=Interface.objects.filter( + domain__in=Domain.objects.filter(name=server_name) + ) + ) + if not query: + return JSONError("This service is not active for this server") + + services = query.all() + seria = ServiceLinkSerializer(services, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def dns_mac_ip_dns(request): + """The list of all active interfaces with all the associated infos + (MAC, IP, IpType, DNS name and associated zone extension) + + Returns: + GET: + A JSON Success response with a field `data` containing: + * a list of dictionnaries (one for each interface) containing: + * a field `ipv4` containing: + * a field `ipv4`: the ip for this interface + * a field `ip_type`: the name of the IpType of this interface + * a field `ipv6` containing `null` if ipv6 is deactivated else: + * a field `ipv6`: the ip for this interface + * a field `ip_type`: the name of the IpType of this interface + * a field `mac_address`: the MAC of this interface + * a field `domain`: the DNS name for this interface + * a field `extension`: the extension for the DNS zone of this interface + """ + interfaces = all_active_assigned_interfaces(full=True) + seria = FullInterfaceSerializer(interfaces, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def dns_alias(request): + """The list of all the alias used and the DNS info associated + + Returns: + GET: + A JSON Success response with a field `data` containing: + * a list of dictionnaries (one for each alias) containing: + * a field `name`: the alias used + * a field `cname`: the target of the alias (real name of the interface) + * a field `cname_entry`: the entry to write in the DNS to have the alias + * a field `extension`: the extension for the DNS zone of this interface + """ + alias = Domain.objects.filter(interface_parent=None).filter(cname__in=Domain.objects.filter(interface_parent__in=Interface.objects.exclude(ipv4=None))).select_related('extension').select_related('cname__extension') + seria = DomainSerializer(alias, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def dns_corresp(request): + """The list of the IpTypes possible with the infos about each + + Returns: + GET: + A JSON Success response with a field `data` containing: + * a list of dictionnaries (one for each IpType) containing: + * a field `type`: the name of the type + * a field `extension`: the DNS extension associated + * a field `domain_ip_start`: the first ip to use for this type + * a field `domain_ip_stop`: the last ip to use for this type + * a field `prefix_v6`: `null` if IPv6 is deactivated else the prefix to use + * a field `ouverture_ports_tcp_in`: the policy for TCP IN ports + * a field `ouverture_ports_tcp_out`: the policy for TCP OUT ports + * a field `ouverture_ports_udp_in`: the policy for UDP IN ports + * a field `ouverture_ports_udp_out`: the policy for UDP OUT ports + """ + ip_type = IpType.objects.all().select_related('extension') + seria = TypeSerializer(ip_type, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def dns_mx(request): + """The list of MX record to add to the DNS + + Returns: + GET: + A JSON Success response with a field `data` containing: + * a list of dictionnaries (one for each MX record) containing: + * a field `zone`: the extension for the concerned zone + * a field `priority`: the priority to use + * a field `name`: the name of the target + * a field `mx_entry`: the full entry to add in the DNS for this MX record + """ + mx = Mx.objects.all().select_related('zone').select_related('name__extension') + seria = MxSerializer(mx, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def dns_ns(request): + """The list of NS record to add to the DNS + + Returns: + GET: + A JSON Success response with a field `data` containing: + * a list of dictionnaries (one for each NS record) containing: + * a field `zone`: the extension for the concerned zone + * a field `ns`: the DNS name for the NS server targeted + * a field `ns_entry`: the full entry to add in the DNS for this NS record + """ + ns = Ns.objects.exclude(ns__in=Domain.objects.filter(interface_parent__in=Interface.objects.filter(ipv4=None))).select_related('zone').select_related('ns__extension') + seria = NsSerializer(ns, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def dns_txt(request): + """The list of TXT record to add to the DNS + + Returns: + GET: + A JSON Success response with a field `data` containing: + * a list of dictionnaries (one for each TXT record) containing: + * a field `zone`: the extension for the concerned zone + * a field `field1`: the first field in the record (target) + * a field `field2`: the second field in the record (value) + * a field `txt_entry`: the full entry to add in the DNS for this TXT record + """ + txt = Txt.objects.all().select_related('zone') + seria = TxtSerializer(txt, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def dns_srv(request): + """The list of SRV record to add to the DNS + + Returns: + GET: + A JSON Success response with a field `data` containing: + * a list of dictionnaries (one for each SRV record) containing: + * a field `extension`: the extension for the concerned zone + * a field `service`: the name of the service concerned + * a field `protocole`: the name of the protocol to use + * a field `ttl`: the Time To Live to use + * a field `priority`: the priority for this service + * a field `weight`: the weight for same priority entries + * a field `port`: the port targeted + * a field `target`: the interface targeted by this service + * a field `srv_entry`: the full entry to add in the DNS for this SRV record + """ + srv = Srv.objects.all().select_related('extension').select_related('target__extension') + seria = SrvSerializer(srv, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def dns_zones(request): + """The list of the zones managed + + Returns: + GET: + A JSON Success response with a field `data` containing: + * a list of dictionnaries (one for each zone) containing: + * a field `name`: the extension for the zone + * a field `origin`: the server IPv4 for the orgin of the zone + * a field `origin_v6`: `null` if ipv6 is deactivated else the server IPv6 for the origin of the zone + * a field `soa` containing: + * a field `mail` containing the mail to contact in case of problem with the zone + * a field `param` containing the full soa paramters to use in the DNS for this zone + * a field `zone_entry`: the full entry to add in the DNS for the origin of the zone + """ + zones = Extension.objects.all().select_related('origin') + seria = ExtensionSerializer(zones, many=True) + return JSONSuccess(seria.data) + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def firewall_ouverture_ports(request): + """The list of the ports authorized to be openned by the firewall + + Returns: + GET: + A JSONSuccess response with a `data` field containing: + * a field `ipv4` containing: + * a field `tcp_in` containing: + * a list of port number where ipv4 tcp in should be ok + * a field `tcp_out` containing: + * a list of port number where ipv4 tcp ou should be ok + * a field `udp_in` containing: + * a list of port number where ipv4 udp in should be ok + * a field `udp_out` containing: + * a list of port number where ipv4 udp out should be ok + * a field `ipv6` containing: + * a field `tcp_in` containing: + * a list of port number where ipv6 tcp in should be ok + * a field `tcp_out` containing: + * a list of port number where ipv6 tcp ou should be ok + * a field `udp_in` containing: + * a list of port number where ipv6 udp in should be ok + * a field `udp_out` containing: + * a list of port number where ipv6 udp out should be ok + """ + r = {'ipv4':{}, 'ipv6':{}} + for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set', 'interface_set__ipv4'): + pl = { + "tcp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN))), + "tcp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT))), + "udp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN))), + "udp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT))), + } + for i in filter_active_interfaces(o.interface_set): + if i.may_have_port_open(): + d = r['ipv4'].get(i.ipv4.ipv4, {}) + d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) + d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) + d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) + d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + r['ipv4'][i.ipv4.ipv4] = d + if i.ipv6(): + for ipv6 in i.ipv6(): + d = r['ipv6'].get(ipv6.ipv6, {}) + d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) + d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) + d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) + d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + r['ipv6'][ipv6.ipv6] = d + return JSONSuccess(r) + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def dhcp_mac_ip(request): + """The list of all active interfaces with all the associated infos + (MAC, IP, IpType, DNS name and associated zone extension) + + Returns: + GET: + A JSON Success response with a field `data` containing: + * a list of dictionnaries (one for each interface) containing: + * a field `ipv4` containing: + * a field `ipv4`: the ip for this interface + * a field `ip_type`: the name of the IpType of this interface + * a field `mac_address`: the MAC of this interface + * a field `domain`: the DNS name for this interface + * a field `extension`: the extension for the DNS zone of this interface + """ + interfaces = all_active_assigned_interfaces() + seria = InterfaceSerializer(interfaces, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def mailing_standard(request): + """All the available standard mailings. + + Returns: + GET: + A JSONSucess response with a field `data` containing: + * a list of dictionnaries (one for each mailing) containing: + * a field `name`: the name of a mailing + """ + return JSONSuccess([ + {'name': 'adherents'} + ]) + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def mailing_standard_ml_members(request): + """All the members of a specific standard mailing + + Returns: + GET: + A JSONSucess response with a field `data` containing: + * a list if dictionnaries (one for each member) containing: + * a field `email`: the email of the member + * a field `name`: the name of the member + * a field `surname`: the surname of the member + * a field `pseudo`: the pseudo of the member + """ + # All with active connextion + if ml_name == 'adherents': + members = all_has_access().values('email').distinct() + # Unknown mailing + else: + return JSONError("This mailing does not exist") + seria = MailingMemberSerializer(members, many=True) + return JSONSuccess(seria.data) + + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def mailing_club(request): + """All the available club mailings. + + Returns: + GET: + A JSONSucess response with a field `data` containing: + * a list of dictionnaries (one for each mailing) containing: + * a field `name` indicating the name of a mailing + """ + clubs = Club.objects.filter(mailing=True).values('pseudo') + seria = MailingSerializer(clubs, many=True) + return JSONSuccess(seria.data) + +@csrf_exempt +@login_required +@permission_required('machines.serveur') +@accept_method(['GET']) +def mailing_club_ml_members(request): + """All the members of a specific club mailing + + Returns: + GET: + A JSONSucess response with a field `data` containing: + * a list if dictionnaries (one for each member) containing: + * a field `email`: the email of the member + * a field `name`: the name of the member + * a field `surname`: the surname of the member + * a field `pseudo`: the pseudo of the member + """ + try: + club = Club.objects.get(mailing=True, pseudo=ml_name) + except Club.DoesNotExist: + return JSONError("This mailing does not exist") + members = club.administrators.all().values('email').distinct() + seria = MailingMemberSerializer(members, many=True) + return JSONSuccess(seria.data) diff --git a/contributors.py b/contributors.py new file mode 100644 index 00000000..02a2015c --- /dev/null +++ b/contributors.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +contributeurs = ['Gabriel Detraz', 'chirac', 'Maël Kervella', 'LEVY-FALK Hugo', 'Dalahro', 'lhark', 'root', 'Hugo LEVY-FALK', 'Chirac', 'guimoz', 'Mael Kervella', 'klafyvel', 'matthieu', 'Yoann Pietri', 'Simon Brélivet', 'chibrac', 'David Sinquin', 'Pierre Cadart', 'moamoak', 'Éloi Alain', 'FERNET Laouen', 'Hugo Levy-Falk', 'Joanne Steiner', 'Matthieu Michelet', 'Yoann PIETRI', 'B', 'Daniel STAN', 'Eloi Alain', 'Guimoz', 'Hugo Hervieux', 'Laouen Fernet', 'Lemesle', 'MICHELET matthieu', 'Nymous', 'Thibault de BOUTRAY', 'Tipunchetrhum', 'Éloi ALAIN'] \ No newline at end of file diff --git a/cotisations/__init__.py b/cotisations/__init__.py index fc1be5d7..df6e4256 100644 --- a/cotisations/__init__.py +++ b/cotisations/__init__.py @@ -21,3 +21,4 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from .acl import * diff --git a/cotisations/acl.py b/cotisations/acl.py new file mode 100644 index 00000000..868e3411 --- /dev/null +++ b/cotisations/acl.py @@ -0,0 +1,40 @@ +# -*- mode: python; coding: utf-8 -*- +# 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. + +"""cotisations.acl + +Here are defined some functions to check acl on the application. +""" + +def can_view(user): + """Check if an user can view the application. + + Args: + user: The user who wants to view the application. + + Returns: + A couple (allowed, msg) where allowed is a boolean which is True if + viewing is granted and msg is a message (can be None). + """ + can = user.has_module_perms('cotisations') + return can, None if can else "Vous ne pouvez pas voir cette application." diff --git a/cotisations/forms.py b/cotisations/forms.py index 7725016c..ba4c066f 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -26,9 +26,8 @@ importé par les views. Permet de créer une nouvelle facture pour un user (NewFactureForm), et de l'editer (soit l'user avec EditFactureForm, soit le trésorier avec TrezEdit qui a plus de possibilités que self -notamment sur le controle trésorier) - -SelectArticleForm est utilisée lors de la creation d'une facture en +notamment sur le controle trésorier SelectArticleForm est utilisée +lors de la creation d'une facture en parrallèle de NewFacture pour le choix des articles désirés. (la vue correspondante est unique) @@ -40,8 +39,12 @@ from __future__ import unicode_literals from django import forms from django.db.models import Q from django.forms import ModelForm, Form -from django.core.validators import MinValueValidator +from django.core.validators import MinValueValidator,MaxValueValidator from .models import Article, Paiement, Facture, Banque +from preferences.models import OptionalUser +from users.models import User + +from re2o.field_permissions import FieldPermissionFormMixin class NewFactureForm(ModelForm): @@ -141,27 +144,18 @@ class NewFactureFormPdf(Form): ) -class EditFactureForm(NewFactureForm): +class EditFactureForm(FieldPermissionFormMixin, NewFactureForm): """Edition d'une facture : moyen de paiement, banque, user parent""" class Meta(NewFactureForm.Meta): - fields = ['paiement', 'banque', 'cheque', 'user'] + model = Facture + fields = '__all__' def __init__(self, *args, **kwargs): super(EditFactureForm, self).__init__(*args, **kwargs) self.fields['user'].label = 'Adherent' self.fields['user'].empty_label = "Séléctionner\ l'adhérent propriétaire" - - -class TrezEditFactureForm(EditFactureForm): - """Vue pour édition controle trésorier""" - class Meta(EditFactureForm.Meta): - fields = '__all__' - - def __init__(self, *args, **kwargs): - super(TrezEditFactureForm, self).__init__(*args, **kwargs) self.fields['valid'].label = 'Validité de la facture' - self.fields['control'].label = 'Contrôle de la facture' class ArticleForm(ModelForm): @@ -180,11 +174,19 @@ class DelArticleForm(Form): """Suppression d'un ou plusieurs articles en vente. Choix parmis les modèles""" articles = forms.ModelMultipleChoiceField( - queryset=Article.objects.all(), + queryset=Article.objects.none(), label="Articles actuels", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelArticleForm, self).__init__(*args, **kwargs) + if instances: + self.fields['articles'].queryset = instances + else: + self.fields['articles'].queryset = Article.objects.all() + class PaiementForm(ModelForm): """Creation d'un moyen de paiement, champ text moyen et type @@ -204,11 +206,19 @@ class DelPaiementForm(Form): """Suppression d'un ou plusieurs moyens de paiements, selection parmis les models""" paiements = forms.ModelMultipleChoiceField( - queryset=Paiement.objects.all(), + queryset=Paiement.objects.none(), label="Moyens de paiement actuels", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelPaiementForm, self).__init__(*args, **kwargs) + if instances: + self.fields['paiements'].queryset = instances + else: + self.fields['paiements'].queryset = Paiement.objects.all() + class BanqueForm(ModelForm): """Creation d'une banque, field name""" @@ -225,7 +235,69 @@ class BanqueForm(ModelForm): class DelBanqueForm(Form): """Selection d'une ou plusieurs banques, pour suppression""" banques = forms.ModelMultipleChoiceField( - queryset=Banque.objects.all(), + queryset=Banque.objects.none(), label="Banques actuelles", widget=forms.CheckboxSelectMultiple ) + + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelBanqueForm, self).__init__(*args, **kwargs) + if instances: + self.fields['banques'].queryset = instances + else: + self.fields['banques'].queryset = Banque.objects.all() + + +class NewFactureSoldeForm(NewFactureForm): + """Creation d'une facture, moyen de paiement, banque et numero + de cheque""" + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + self.fields['cheque'].required = False + self.fields['banque'].required = False + self.fields['cheque'].label = 'Numero de chèque' + self.fields['banque'].empty_label = "Non renseigné" + self.fields['paiement'].empty_label = "Séléctionner\ + une bite de paiement" + paiement_list = Paiement.objects.filter(type_paiement=1) + if paiement_list: + self.fields['paiement'].widget\ + .attrs['data-cheque'] = paiement_list.first().id + + class Meta: + model = Facture + fields = ['paiement', 'banque'] + + + def clean(self): + cleaned_data = super(NewFactureSoldeForm, self).clean() + paiement = cleaned_data.get("paiement") + cheque = cleaned_data.get("cheque") + banque = cleaned_data.get("banque") + if not paiement: + raise forms.ValidationError("Le moyen de paiement est obligatoire") + elif paiement.type_paiement == "check" and not (cheque and banque): + raise forms.ValidationError("Le numéro de chèque et\ + la banque sont obligatoires.") + return cleaned_data + + +class RechargeForm(Form): + value = forms.FloatField( + label='Valeur', + min_value=0.01, + validators = [] + ) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user') + super(RechargeForm, self).__init__(*args, **kwargs) + + def clean_value(self): + value = self.cleaned_data['value'] + if value < OptionalUser.get_cached_value('min_online_payment'): + raise forms.ValidationError("Montant inférieur au montant minimal de paiement en ligne (%s) €" % OptionalUser.get_cached_value('min_online_payment')) + if value + self.user.solde > OptionalUser.get_cached_value('max_solde'): + raise forms.ValidationError("Le solde ne peux excéder %s " % OptionalUser.get_cached_value('max_solde')) + return value diff --git a/cotisations/migrations/0026_auto_20171028_0126.py b/cotisations/migrations/0026_auto_20171028_0126.py index 436e0574..df44bb89 100644 --- a/cotisations/migrations/0026_auto_20171028_0126.py +++ b/cotisations/migrations/0026_auto_20171028_0126.py @@ -59,7 +59,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='cotisation', name='type_cotisation', - field=models.CharField(choices=[('Connexion', 'Connexion'), ('Adhesion', 'Adhesion'), ('All', 'All')], max_length=255), + field=models.CharField(choices=[('Connexion', 'Connexion'), ('Adhesion', 'Adhesion'), ('All', 'All')], max_length=255, default='All'), ), migrations.AddField( model_name='vente', diff --git a/cotisations/migrations/0028_auto_20171231_0007.py b/cotisations/migrations/0028_auto_20171231_0007.py new file mode 100644 index 00000000..0d492489 --- /dev/null +++ b/cotisations/migrations/0028_auto_20171231_0007.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-30 23:07 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0027_auto_20171029_1156'), + ] + + operations = [ + migrations.AlterModelOptions( + name='article', + options={'permissions': (('view_article', 'Peut voir un objet article'),)}, + ), + migrations.AlterModelOptions( + name='banque', + options={'permissions': (('view_banque', 'Peut voir un objet banque'),)}, + ), + migrations.AlterModelOptions( + name='cotisation', + options={'permissions': (('view_cotisation', 'Peut voir un objet cotisation'), ('change_all_cotisation', 'Superdroit, peut modifier toutes les cotisations'))}, + ), + migrations.AlterModelOptions( + name='facture', + options={'permissions': (('change_facture_control', "Peut changer l'etat de controle"), ('change_facture_pdf', 'Peut éditer une facture pdf'), ('view_facture', 'Peut voir un objet facture'), ('change_all_facture', 'Superdroit, peut modifier toutes les factures'))}, + ), + migrations.AlterModelOptions( + name='paiement', + options={'permissions': (('view_paiement', 'Peut voir un objet paiement'),)}, + ), + migrations.AlterModelOptions( + name='vente', + options={'permissions': (('view_vente', 'Peut voir un objet vente'), ('change_all_vente', 'Superdroit, peut modifier toutes les ventes'))}, + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index 090636be..11df1712 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -56,8 +56,10 @@ from django.db.models import Max from django.utils import timezone from machines.models import regen +from re2o.field_permissions import FieldPermissionModelMixin -class Facture(models.Model): + +class Facture(FieldPermissionModelMixin, models.Model): """ Définition du modèle des factures. Une facture regroupe une ou plusieurs ventes, rattachée à un user, et reliée à un moyen de paiement et si il y a lieu un numero pour les chèques. Possède les valeurs @@ -76,6 +78,15 @@ class Facture(models.Model): valid = models.BooleanField(default=True) control = models.BooleanField(default=False) + class Meta: + abstract = False + permissions = ( + ("change_facture_control", "Peut changer l'etat de controle"), + ("change_facture_pdf", "Peut éditer une facture pdf"), + ("view_facture", "Peut voir un objet facture"), + ("change_all_facture", "Superdroit, peut modifier toutes les factures"), + ) + def prix(self): """Renvoie le prix brut sans les quantités. Méthode dépréciée""" @@ -103,6 +114,65 @@ class Facture(models.Model): ).values_list('name', flat=True)) return name + def get_instance(factureid, *args, **kwargs): + return Facture.objects.get(pk=factureid) + + def can_create(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.add_facture'), u"Vous n'avez pas le\ + droit de créer des factures" + + def can_edit(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.change_facture'): + return False, u"Vous n'avez pas le droit d'éditer les factures" + elif not user_request.has_perm('cotisations.change_all_facture') and not self.user.can_edit(user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer les factures de cet user protégé" + elif not user_request.has_perm('cotisations.change_all_facture') and\ + (self.control or not self.valid): + return False, u"Vous n'avez pas le droit d'éditer une facture\ + controlée ou invalidée par un trésorier" + else: + return True, None + + def can_delete(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.delete_facture'): + return False, u"Vous n'avez pas le droit de supprimer une facture" + if not self.user.can_edit(user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer les factures de cet user protégé" + if self.control or not self.valid: + return False, u"Vous ne pouvez pas supprimer une facture\ + contrôlée ou invalidée par un trésorier" + else: + return True, None + + def can_view_all(user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.view_facture'): + return False, u"Vous n'avez pas le droit de voir les factures" + return True, None + + def can_view(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.view_facture') and\ + self.user != user_request: + return False, u"Vous ne pouvez pas afficher l'historique d'une\ + facture d'un autre user que vous sans droit cableur" + elif not self.valid: + return False, u"La facture est invalidée et ne peut être affichée" + else: + return True, None + + @staticmethod + def can_change_control(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.change_facture_control'), "Vous ne pouvez pas éditer le controle sans droit trésorier" + + @staticmethod + def can_change_pdf(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.change_facture_pdf'), "Vous ne pouvez pas éditer une facture sans droit trésorier" + + def __init__(self, *args, **kwargs): + super(Facture, self).__init__(*args, **kwargs) + self.field_permissions = { + 'control' : self.can_change_control, + } + def __str__(self): return str(self.user) + ' ' + str(self.date) @@ -149,6 +219,12 @@ class Vente(models.Model): max_length=255 ) + class Meta: + permissions = ( + ("view_vente", "Peut voir un objet vente"), + ("change_all_vente", "Superdroit, peut modifier toutes les ventes"), + ) + def prix_total(self): """Renvoie le prix_total de self (nombre*prix)""" return self.prix*self.number @@ -201,6 +277,50 @@ class Vente(models.Model): self.update_cotisation() super(Vente, self).save(*args, **kwargs) + def get_instance(venteid, *args, **kwargs): + return Vente.objects.get(pk=venteid) + + def can_create(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.add_vente'), u"Vous n'avez pas le\ + droit de créer des ventes" + return True, None + + def can_edit(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.change_vente'): + return False, u"Vous n'avez pas le droit d'éditer les ventes" + elif not user_request.has_perm('cotisations.change_all_facture') and not self.facture.user.can_edit(user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer les factures de cet user protégé" + elif not user_request.has_perm('cotisations.change_all_vente') and\ + (self.facture.control or not self.facture.valid): + return False, u"Vous n'avez pas le droit d'éditer une vente\ + controlée ou invalidée par un trésorier" + else: + return True, None + + def can_delete(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.delete_vente'): + return False, u"Vous n'avez pas le droit de supprimer une vente" + if not self.facture.user.can_edit(user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer les factures de cet user protégé" + if self.facture.control or not self.facture.valid: + return False, u"Vous ne pouvez pas supprimer une vente\ + contrôlée ou invalidée par un trésorier" + else: + return True, None + + def can_view_all(user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.view_vente'): + return False, u"Vous n'avez pas le droit de voir les ventes" + return True, None + + def can_view(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.view_vente') and\ + self.facture.user != user_request: + return False, u"Vous ne pouvez pas afficher l'historique d'une\ + facture d'un autre user que vous sans droit cableur" + else: + return True, None + def __str__(self): return str(self.name) + ' ' + str(self.facture) @@ -269,6 +389,11 @@ class Article(models.Model): unique_together = ('name', 'type_user') + class Meta: + permissions = ( + ("view_article", "Peut voir un objet article"), + ) + def clean(self): if self.name.lower() == "solde": raise ValidationError("Solde est un nom d'article invalide") @@ -277,6 +402,29 @@ class Article(models.Model): "La durée est obligatoire si il s'agit d'une cotisation" ) + def get_instance(articleid, *args, **kwargs): + return Article.objects.get(pk=articleid) + + def can_create(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.add_article'), u"Vous n'avez pas le\ + droit d'ajouter des articles" + + def can_edit(self, user_request, *args, **kwargs): + return user_request.has_perm('cotisations.change_article'), u"Vous n'avez pas le\ + droit d'éditer des articles" + + def can_delete(self, user_request, *args, **kwargs): + return user_request.has_perm('cotisations.delete_article'), u"Vous n'avez pas le\ + droit de supprimer des articles" + + def can_view_all(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.view_article'), u"Vous n'avez pas le\ + droit de voir des articles" + + def can_view(self, user_request, *args, **kwargs): + return user_request.has_perm('cotisations.view_article'), u"Vous n'avez pas le\ + droit de voir des articles" + def __str__(self): return self.name @@ -287,6 +435,34 @@ class Banque(models.Model): name = models.CharField(max_length=255) + class Meta: + permissions = ( + ("view_banque", "Peut voir un objet banque"), + ) + + def get_instance(banqueid, *args, **kwargs): + return Banque.objects.get(pk=banqueid) + + def can_create(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.add_banque'), u"Vous n'avez pas le\ + droit d'ajouter des banques" + + def can_edit(self, user_request, *args, **kwargs): + return user_request.has_perm('cotisations.change_banque'), u"Vous n'avez pas le\ + droit d'éditer des banques" + + def can_delete(self, user_request, *args, **kwargs): + return user_request.has_perm('cotisations.delete_banque'), u"Vous n'avez pas le\ + droit de supprimer des banques" + + def can_view_all(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.view_banque'), u"Vous n'avez pas le\ + droit de voir des banques" + + def can_view(self, user_request, *args, **kwargs): + return user_request.has_perm('cotisations.view_banque'), u"Vous n'avez pas le\ + droit de voir des banques" + def __str__(self): return self.name @@ -302,6 +478,34 @@ class Paiement(models.Model): moyen = models.CharField(max_length=255) type_paiement = models.IntegerField(choices=PAYMENT_TYPES, default=0) + class Meta: + permissions = ( + ("view_paiement", "Peut voir un objet paiement"), + ) + + def get_instance(paiementid, *args, **kwargs): + return Paiement.objects.get(pk=paiementid) + + def can_create(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.add_paiement'), u"Vous n'avez pas le\ + droit d'ajouter des paiements" + + def can_edit(self, user_request, *args, **kwargs): + return user_request.has_perm('cotisations.change_paiement'), u"Vous n'avez pas le\ + droit d'éditer des paiements" + + def can_delete(self, user_request, *args, **kwargs): + return user_request.has_perm('cotisations.delete_paiement'), u"Vous n'avez pas le\ + droit de supprimer des paiements" + + def can_view_all(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.view_paiement'), u"Vous n'avez pas le\ + droit de voir des paiements" + + def can_view(self, user_request, *args, **kwargs): + return user_request.has_perm('cotisations.view_paiement'), u"Vous n'avez pas le\ + droit de voir des paiements" + def __str__(self): return self.moyen @@ -330,10 +534,57 @@ class Cotisation(models.Model): type_cotisation = models.CharField( choices=COTISATION_TYPE, max_length=255, + default='All', ) date_start = models.DateTimeField() date_end = models.DateTimeField() + class Meta: + permissions = ( + ("view_cotisation", "Peut voir un objet cotisation"), + ("change_all_cotisation", "Superdroit, peut modifier toutes les cotisations"), + ) + + def get_instance(cotisationid, *args, **kwargs): + return Cotisations.objects.get(pk=cotisationid) + + def can_create(user_request, *args, **kwargs): + return user_request.has_perm('cotisations.add_cotisation'), u"Vous n'avez pas le\ + droit de créer des cotisations" + return True, None + + def can_edit(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.change_cotisation'): + return False, u"Vous n'avez pas le droit d'éditer les cotisations" + elif not user_request.has_perm('cotisations.change_all_cotisation') and\ + (self.vente.facture.control or not self.vente.facture.valid): + return False, u"Vous n'avez pas le droit d'éditer une cotisation\ + controlée ou invalidée par un trésorier" + else: + return True, None + + def can_delete(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.delete_cotisation'): + return False, u"Vous n'avez pas le droit de supprimer une cotisations" + if self.vente.facture.control or not self.vente.facture.valid: + return False, u"Vous ne pouvez pas supprimer une cotisations\ + contrôlée ou invalidée par un trésorier" + else: + return True, None + + def can_view_all(user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.view_cotisation'): + return False, u"Vous n'avez pas le droit de voir les cotisations" + return True, None + + def can_view(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.view_cotisation') and\ + self.vente.facture.user != user_request: + return False, u"Vous ne pouvez pas afficher l'historique d'une\ + cotisation d'un autre user que vous sans droit cableur" + else: + return True, None + def __str__(self): return str(self.vente) diff --git a/cotisations/payment.py b/cotisations/payment.py new file mode 100644 index 00000000..f9a66bf6 --- /dev/null +++ b/cotisations/payment.py @@ -0,0 +1,111 @@ +"""Payment + +Here are defined some views dedicated to online payement. +""" +from django.urls import reverse +from django.shortcuts import redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.views.decorators.csrf import csrf_exempt +from django.utils.datastructures import MultiValueDictKeyError +from django.http import HttpResponse, HttpResponseBadRequest + +from collections import OrderedDict + +from preferences.models import AssoOption +from .models import Facture +from .payment_utils.comnpay import Payment as ComnpayPayment + +@csrf_exempt +@login_required +def accept_payment(request, factureid): + facture = get_object_or_404(Facture, id=factureid) + messages.success( + request, + "Le paiement de {} € a été accepté.".format(facture.prix()) + ) + return redirect(reverse('users:profil', kwargs={'userid':request.user.id})) + + +@csrf_exempt +@login_required +def refuse_payment(request): + messages.error( + request, + "Le paiement a été refusé." + ) + return redirect(reverse('users:profil', kwargs={'userid':request.user.id})) + +@csrf_exempt +def ipn(request): + p = ComnpayPayment() + order = ('idTpe', 'idTransaction', 'montant', 'result', 'sec', ) + try: + data = OrderedDict([(f, request.POST[f]) for f in order]) + except MultiValueDictKeyError: + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + if not p.validSec(data, AssoOption.get_cached_value('payment_pass')): + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + result = True if (request.POST['result'] == 'OK') else False + idTpe = request.POST['idTpe'] + idTransaction = request.POST['idTransaction'] + + # On vérifie que le paiement nous est destiné + if not idTpe == AssoOption.get_cached_value('payment_id'): + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + try: + factureid = int(idTransaction) + except ValueError: + return HttpResponseBadRequest("HTTP/1.1 400 Bad Request") + + facture = get_object_or_404(Facture, id=factureid) + + # On vérifie que le paiement est valide + if not result: + # Le paiement a échoué : on effectue les actions nécessaires (On indique qu'elle a échoué) + facture.delete() + + # On notifie au serveur ComNPay qu'on a reçu les données pour traitement + return HttpResponse("HTTP/1.1 200 OK") + + facture.valid = True + facture.save() + + # A nouveau, on notifie au serveur qu'on a bien traité les données + return HttpResponse("HTTP/1.0 200 OK") + + +def comnpay(facture, request): + host = request.get_host() + p = ComnpayPayment( + str(AssoOption.get_cached_value('payment_id')), + str(AssoOption.get_cached_value('payment_pass')), + 'https://' + host + reverse( + 'cotisations:accept_payment', + kwargs={'factureid':facture.id} + ), + 'https://' + host + reverse('cotisations:refuse_payment'), + 'https://' + host + reverse('cotisations:ipn'), + "", + "D" + ) + r = { + 'action' : 'https://secure.homologation.comnpay.com', + 'method' : 'POST', + 'content' : p.buildSecretHTML( + "Rechargement du solde", + facture.prix(), + idTransaction=str(facture.id) + ), + 'amount' : facture.prix, + } + return r + + +PAYMENT_SYSTEM = { + 'COMNPAY' : comnpay, + 'NONE' : None +} diff --git a/cotisations/payment_utils/__init__.py b/cotisations/payment_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cotisations/payment_utils/comnpay.py b/cotisations/payment_utils/comnpay.py new file mode 100644 index 00000000..6c1701d3 --- /dev/null +++ b/cotisations/payment_utils/comnpay.py @@ -0,0 +1,68 @@ +import time +from random import randrange +import base64 +import hashlib +from collections import OrderedDict +from itertools import chain + +class Payment(): + + vad_number = "" + secret_key = "" + urlRetourOK = "" + urlRetourNOK = "" + urlIPN = "" + source = "" + typeTr = "D" + + def __init__(self, vad_number = "", secret_key = "", urlRetourOK = "", urlRetourNOK = "", urlIPN = "", source="", typeTr="D"): + self.vad_number = vad_number + self.secret_key = secret_key + self.urlRetourOK = urlRetourOK + self.urlRetourNOK = urlRetourNOK + self.urlIPN = urlIPN + self.source = source + self.typeTr = typeTr + + def buildSecretHTML(self, produit="Produit", montant="0.00", idTransaction=""): + if idTransaction == "": + self.idTransaction = str(time.time())+self.vad_number+str(randrange(999)) + else: + self.idTransaction = idTransaction + + array_tpe = OrderedDict( + montant= str(montant), + idTPE= self.vad_number, + idTransaction= self.idTransaction, + devise= "EUR", + lang= 'fr', + nom_produit= produit, + source= self.source, + urlRetourOK= self.urlRetourOK, + urlRetourNOK= self.urlRetourNOK, + typeTr= str(self.typeTr) + ) + + if self.urlIPN!="": + array_tpe['urlIPN'] = self.urlIPN + + array_tpe['key'] = self.secret_key; + strWithKey = base64.b64encode(bytes('|'.join(array_tpe.values()), 'utf-8')) + del array_tpe["key"] + array_tpe['sec'] = hashlib.sha512(strWithKey).hexdigest() + + ret = "" + for key in array_tpe: + ret += '' + + return ret + + def validSec(self, values, secret_key): + if "sec" in values: + sec = values['sec'] + del values["sec"] + strWithKey = hashlib.sha512(base64.b64encode(bytes('|'.join(values.values()) +"|"+secret_key, 'utf-8'))).hexdigest() + return strWithKey.upper() == sec.upper() + else: + return False + diff --git a/cotisations/templates/cotisations/aff_article.html b/cotisations/templates/cotisations/aff_article.html index 3a0b21f6..833b7de0 100644 --- a/cotisations/templates/cotisations/aff_article.html +++ b/cotisations/templates/cotisations/aff_article.html @@ -22,6 +22,8 @@ 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 %} + @@ -41,13 +43,13 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/cotisations/templates/cotisations/aff_banque.html b/cotisations/templates/cotisations/aff_banque.html index fd962f1f..1ef4cb76 100644 --- a/cotisations/templates/cotisations/aff_banque.html +++ b/cotisations/templates/cotisations/aff_banque.html @@ -22,6 +22,8 @@ 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 %} +
{{ article.duration }} {{ article.type_user }} - {% if is_trez %} + {% can_edit article %} - + - {% endif %} + {% acl_end %} - +
@@ -33,13 +35,13 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/cotisations/templates/cotisations/aff_cotisations.html b/cotisations/templates/cotisations/aff_cotisations.html index 34efd63d..b73bc107 100644 --- a/cotisations/templates/cotisations/aff_cotisations.html +++ b/cotisations/templates/cotisations/aff_cotisations.html @@ -22,6 +22,8 @@ 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 %} + {% if facture_list.paginator %} {% include "pagination.html" with list=facture_list %} {% endif %} @@ -47,7 +49,6 @@ with this program; if not, write to the Free Software Foundation, Inc., - {% if is_cableur %} - {% endif %} + + + + + +
{{ banque.name }} - {% if is_trez %} + {% can_edit banque %} - + - {% endif %} + {% acl_end %} - +
{{ facture.paiement }} {{ facture.date }} {{ facture.id }} {% if facture.valid %} - + PDF {% else %} diff --git a/cotisations/templates/cotisations/aff_paiement.html b/cotisations/templates/cotisations/aff_paiement.html index 5cf8d176..09e5acc3 100644 --- a/cotisations/templates/cotisations/aff_paiement.html +++ b/cotisations/templates/cotisations/aff_paiement.html @@ -22,6 +22,8 @@ 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 %} + @@ -33,13 +35,13 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/cotisations/templates/cotisations/control.html b/cotisations/templates/cotisations/control.html index baa4938e..48071429 100644 --- a/cotisations/templates/cotisations/control.html +++ b/cotisations/templates/cotisations/control.html @@ -55,8 +55,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% for form in controlform.forms %} {% bootstrap_form_errors form %} - - diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index 9b7f17c9..5ed3e94e 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} {% bootstrap_form factureform %} - {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} + {% bootstrap_button action_name button_type="submit" icon="star" %} {% endblock %} diff --git a/cotisations/templates/cotisations/index_article.html b/cotisations/templates/cotisations/index_article.html index ee9f3db1..7803c2ca 100644 --- a/cotisations/templates/cotisations/index_article.html +++ b/cotisations/templates/cotisations/index_article.html @@ -24,15 +24,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load acl %} {% block title %}Articles{% endblock %} {% block content %}

Liste des types d'articles

- {% if is_trez %} - Ajouter un type d'articles - Supprimer un ou plusieurs types d'articles - {% endif %} + {% can_create Article %} + Ajouter un type d'articles + {% acl_end %} + Supprimer un ou plusieurs types d'articles {% include "cotisations/aff_article.html" with article_list=article_list %}

diff --git a/cotisations/templates/cotisations/index_banque.html b/cotisations/templates/cotisations/index_banque.html index 142c163f..77c23977 100644 --- a/cotisations/templates/cotisations/index_banque.html +++ b/cotisations/templates/cotisations/index_banque.html @@ -24,15 +24,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load acl %} {% block title %}Banques{% endblock %} {% block content %}

Liste des banques

- Ajouter une banque - {% if is_trez %} - Supprimer une ou plusieurs banques - {% endif %} + {% can_create Banque %} + Ajouter une banque + {% acl_end %} + Supprimer une ou plusieurs banques {% include "cotisations/aff_banque.html" with banque_list=banque_list %}

diff --git a/cotisations/templates/cotisations/index_paiement.html b/cotisations/templates/cotisations/index_paiement.html index d2734e46..414c6b38 100644 --- a/cotisations/templates/cotisations/index_paiement.html +++ b/cotisations/templates/cotisations/index_paiement.html @@ -24,15 +24,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load acl %} {% block title %}Paiements{% endblock %} {% block content %}

Liste des types de paiements

- {% if is_trez %} - Ajouter un type de paiement - Supprimer un ou plusieurs types de paiements - {% endif %} + {% can_create Paiement %} + Ajouter un type de paiement + {% acl_end %} + Supprimer un ou plusieurs types de paiements {% include "cotisations/aff_paiement.html" with paiement_list=paiement_list %}

diff --git a/cotisations/templates/cotisations/new_facture.html b/cotisations/templates/cotisations/new_facture.html index f2586e8b..10aa69fd 100644 --- a/cotisations/templates/cotisations/new_facture.html +++ b/cotisations/templates/cotisations/new_facture.html @@ -34,6 +34,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %}

Nouvelle facture

+

+ Solde de l'utilisateur : {{ user.solde }} € +

{% bootstrap_form factureform %} {{ venteform.management_form }} @@ -46,7 +49,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,   {% endfor %} @@ -55,7 +58,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Prix total : 0,00

- {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} + {% bootstrap_button "Créer" button_type="submit" icon="star" %} + +{% endblock %} + diff --git a/cotisations/templates/cotisations/payment.html b/cotisations/templates/cotisations/payment.html new file mode 100644 index 00000000..46f26784 --- /dev/null +++ b/cotisations/templates/cotisations/payment.html @@ -0,0 +1,37 @@ +{% extends "cotisations/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 staticfiles%} + +{% block title %}Rechargement du solde{% endblock %} + +{% block content %} +

Recharger de {{ amount }} €

+ + {{ content | safe }} + {% bootstrap_button "Payer" button_type="submit" icon="piggy-bank" %} + +{% endblock %} diff --git a/cotisations/templates/cotisations/recharge.html b/cotisations/templates/cotisations/recharge.html new file mode 100644 index 00000000..d38b7614 --- /dev/null +++ b/cotisations/templates/cotisations/recharge.html @@ -0,0 +1,39 @@ +{% extends "cotisations/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 staticfiles%} + +{% block title %}Rechargement du solde{% endblock %} + +{% block content %} +

Rechargement du solde

+

Solde : {{ request.user.solde }} €

+ + {% csrf_token %} + {% bootstrap_form rechargeform %} + {% bootstrap_button "Valider" button_type="submit" icon="piggy-bank" %} + +{% endblock %} diff --git a/cotisations/templates/cotisations/sidebar.html b/cotisations/templates/cotisations/sidebar.html index 1860b1ee..513eb60b 100644 --- a/cotisations/templates/cotisations/sidebar.html +++ b/cotisations/templates/cotisations/sidebar.html @@ -23,32 +23,41 @@ 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 %} {% block sidebar %} - {% if is_trez %} + {% can_change Facture pdf %} - + Créer une facture - + Contrôler les factures - {% endif %} + {% acl_end %} + {% can_view_all Facture %} - + Factures + {% acl_end %} + {% can_view_all Article %} - + Articles en vente + {% acl_end %} + {% can_view_all Banque %} - + Banques + {% acl_end %} + {% can_view_all Paiement %} - + Moyens de paiement + {% acl_end %} {% endblock %} diff --git a/cotisations/urls.py b/cotisations/urls.py index d3e56f36..0040e48c 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -24,7 +24,9 @@ from __future__ import unicode_literals from django.conf.urls import url +import re2o from . import views +from . import payment urlpatterns = [ url(r'^new_facture/(?P[0-9]+)$', @@ -99,24 +101,35 @@ urlpatterns = [ views.index_paiement, name='index-paiement' ), - url(r'^history/(?Pfacture)/(?P[0-9]+)$', - views.history, - name='history' - ), - url(r'^history/(?Particle)/(?P[0-9]+)$', - views.history, - name='history' - ), - url(r'^history/(?Ppaiement)/(?P[0-9]+)$', - views.history, - name='history'), - url(r'^history/(?Pbanque)/(?P[0-9]+)$', - views.history, - name='history' - ), + url( + r'history/(?P\w+)/(?P[0-9]+)$', + re2o.views.history, + name='history', + kwargs={'application':'cotisations'}, + ), url(r'^control/$', views.control, name='control' ), + url(r'^new_facture_solde/(?P[0-9]+)$', + views.new_facture_solde, + name='new_facture_solde' + ), + url(r'^recharge/$', + views.recharge, + name='recharge' + ), + url(r'^payment/accept/(?P[0-9]+)$', + payment.accept_payment, + name='accept_payment' + ), + url(r'^payment/refuse/$', + payment.refuse_payment, + name='refuse_payment' + ), + url(r'^payment/ipn/$', + payment.ipn, + name='ipn' + ), url(r'^$', views.index, name='index'), ] diff --git a/cotisations/views.py b/cotisations/views.py index 1a9b8ed9..a9c996d6 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -29,6 +29,7 @@ import os from django.urls import reverse from django.shortcuts import render, redirect from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.core.validators import MaxValueValidator from django.contrib.auth.decorators import login_required, permission_required from django.contrib import messages from django.db.models import ProtectedError @@ -36,6 +37,8 @@ from django.db import transaction from django.db.models import Q from django.forms import modelformset_factory, formset_factory from django.utils import timezone +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.debug import sensitive_variables from reversion import revisions as reversion from reversion.models import Version # Import des models, forms et fonctions re2o @@ -44,11 +47,19 @@ from re2o.settings import LOGO_PATH from re2o import settings from re2o.views import form from re2o.utils import SortTable +from re2o.acl import ( + can_create, + can_edit, + can_delete, + can_view, + can_view_all, + can_delete_set, + can_change, +) from preferences.models import OptionalUser, AssoOption, GeneralOption from .models import Facture, Article, Vente, Paiement, Banque from .forms import ( NewFactureForm, - TrezEditFactureForm, EditFactureForm, ArticleForm, DelArticleForm, @@ -59,14 +70,19 @@ from .forms import ( NewFactureFormPdf, SelectUserArticleForm, SelectClubArticleForm, - CreditSoldeForm + CreditSoldeForm, + NewFactureSoldeForm, + RechargeForm ) +from . import payment from .tex import render_invoice + @login_required -@permission_required('cableur') -def new_facture(request, userid): +@can_create(Facture) +@can_edit(User) +def new_facture(request, user, userid): """Creation d'une facture pour un user. Renvoie la liste des articles et crée des factures dans un formset. Utilise un peu de js coté template pour ajouter des articles. @@ -74,11 +90,6 @@ def new_facture(request, userid): enfin sauve la facture parente. TODO : simplifier cette fonction, déplacer l'intelligence coté models Facture et Vente.""" - try: - user = User.objects.get(pk=userid) - except User.DoesNotExist: - messages.error(request, u"Utilisateur inexistant") - return redirect(reverse('cotisations:index')) facture = Facture(user=user) # Le template a besoin de connaitre les articles pour le js article_list = Article.objects.filter( @@ -95,9 +106,8 @@ def new_facture(request, userid): articles = article_formset # Si au moins un article est rempli if any(art.cleaned_data for art in articles): - options, _created = OptionalUser.objects.get_or_create() - user_solde = options.user_solde - solde_negatif = options.solde_negatif + user_solde = OptionalUser.get_cached_value('user_solde') + solde_negatif = OptionalUser.get_cached_value('solde_negatif') # Si on paye par solde, que l'option est activée, # on vérifie que le négatif n'est pas atteint if user_solde: @@ -163,14 +173,13 @@ def new_facture(request, userid): @login_required -@permission_required('tresorier') +@can_change(Facture, 'pdf') def new_facture_pdf(request): """Permet de générer un pdf d'une facture. Réservée au trésorier, permet d'emettre des factures sans objet Vente ou Facture correspondant en bdd""" facture_form = NewFactureFormPdf(request.POST or None) if facture_form.is_valid(): - options, _created = AssoOption.objects.get_or_create() tbl = [] article = facture_form.cleaned_data['article'] quantite = facture_form.cleaned_data['number'] @@ -189,48 +198,30 @@ def new_facture_pdf(request): 'article': tbl, 'total': prix_total, 'paid': paid, - 'asso_name': options.name, - 'line1': options.adresse1, - 'line2': options.adresse2, - 'siret': options.siret, - 'email': options.contact, - 'phone': options.telephone, + 'asso_name': AssoOption.get_cached_value('name'), + 'line1': AssoOption.get_cached_value('adresse1'), + 'line2': AssoOption.get_cached_value('adresse2'), + 'siret': AssoOption.get_cached_value('siret'), + 'email': AssoOption.get_cached_value('contact'), + 'phone': AssoOption.get_cached_value('telephone'), 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) }) return form({ - 'factureform': facture_form + 'factureform': facture_form, + 'action_name' : 'Editer' }, 'cotisations/facture.html', request) @login_required -def facture_pdf(request, factureid): +@can_view(Facture) +def facture_pdf(request, facture, factureid): """Affiche en pdf une facture. Cree une ligne par Vente de la facture, et génére une facture avec le total, le moyen de paiement, l'adresse de l'adhérent, etc. Réservée à self pour un user sans droits, les droits cableurs permettent d'afficher toute facture""" - try: - facture = Facture.objects.get(pk=factureid) - except Facture.DoesNotExist: - messages.error(request, u"Facture inexistante") - return redirect(reverse('cotisations:index')) - if not request.user.has_perms(('cableur',))\ - and facture.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher une facture ne vous\ - appartenant pas sans droit cableur") - return redirect(reverse( - 'users:profil', - kwargs={'userid': str(request.user.id)} - )) - if not facture.valid: - messages.error(request, "Vous ne pouvez pas afficher\ - une facture non valide") - return redirect(reverse( - 'users:profil', - kwargs={'userid': str(request.user.id)} - )) + ventes_objects = Vente.objects.all().filter(facture=facture) ventes = [] - options, _created = AssoOption.objects.get_or_create() for vente in ventes_objects: ventes.append([vente, vente.number, vente.prix_total]) return render_invoice(request, { @@ -240,38 +231,23 @@ def facture_pdf(request, factureid): 'dest': facture.user, 'article': ventes, 'total': facture.prix_total(), - 'asso_name': options.name, - 'line1': options.adresse1, - 'line2': options.adresse2, - 'siret': options.siret, - 'email': options.contact, - 'phone': options.telephone, + 'asso_name': AssoOption.get_cached_value('name'), + 'line1': AssoOption.get_cached_value('adresse1'), + 'line2': AssoOption.get_cached_value('adresse2'), + 'siret': AssoOption.get_cached_value('siret'), + 'email': AssoOption.get_cached_value('contact'), + 'phone': AssoOption.get_cached_value('telephone'), 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) }) @login_required -@permission_required('cableur') -def edit_facture(request, factureid): +@can_edit(Facture) +def edit_facture(request, facture, factureid): """Permet l'édition d'une facture. On peut y éditer les ventes déjà effectuer, ou rendre une facture invalide (non payées, chèque en bois etc). Mets à jour les durée de cotisation attenantes""" - try: - facture = Facture.objects.get(pk=factureid) - except Facture.DoesNotExist: - messages.error(request, u"Facture inexistante") - return redirect(reverse('cotisations:index')) - if request.user.has_perms(['tresorier']): - facture_form = TrezEditFactureForm( - request.POST or None, - instance=facture - ) - elif facture.control or not facture.valid: - messages.error(request, "Vous ne pouvez pas editer une facture\ - controlée ou invalidée par le trésorier") - return redirect(reverse('cotisations:index')) - else: - facture_form = EditFactureForm(request.POST or None, instance=facture) + facture_form = EditFactureForm(request.POST or None, instance=facture, user=request.user) ventes_objects = Vente.objects.filter(facture=facture) vente_form_set = modelformset_factory( Vente, @@ -297,19 +273,10 @@ def edit_facture(request, factureid): @login_required -@permission_required('cableur') -def del_facture(request, factureid): +@can_delete(Facture) +def del_facture(request, facture, factureid): """Suppression d'une facture. Supprime en cascade les ventes et cotisations filles""" - try: - facture = Facture.objects.get(pk=factureid) - except Facture.DoesNotExist: - messages.error(request, u"Facture inexistante") - return redirect(reverse('cotisations:index')) - if facture.control or not facture.valid: - messages.error(request, "Vous ne pouvez pas editer une facture\ - controlée ou invalidée par le trésorier") - return redirect(reverse('cotisations:index')) if request.method == "POST": with transaction.atomic(), reversion.create_revision(): facture.delete() @@ -323,14 +290,10 @@ def del_facture(request, factureid): @login_required -@permission_required('cableur') -def credit_solde(request, userid): +@can_create(Facture) +@can_edit(User) +def credit_solde(request, user, userid): """ Credit ou débit de solde """ - try: - user = User.objects.get(pk=userid) - except User.DoesNotExist: - messages.error(request, u"Utilisateur inexistant") - return redirect(reverse('cotisations:index')) facture = CreditSoldeForm(request.POST or None) if facture.is_valid(): facture_instance = facture.save(commit=False) @@ -351,11 +314,11 @@ def credit_solde(request, userid): reversion.set_comment("Création") messages.success(request, "Solde modifié") return redirect(reverse('cotisations:index')) - return form({'factureform': facture}, 'cotisations/facture.html', request) + return form({'factureform': facture, 'action_name' : 'Créditer'}, 'cotisations/facture.html', request) @login_required -@permission_required('tresorier') +@can_create(Article) def add_article(request): """Ajoute un article. Champs : désignation, prix, est-ce une cotisation et si oui sa durée @@ -372,19 +335,14 @@ def add_article(request): reversion.set_comment("Création") messages.success(request, "L'article a été ajouté") return redirect(reverse('cotisations:index-article')) - return form({'factureform': article}, 'cotisations/facture.html', request) + return form({'factureform': article, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request) @login_required -@permission_required('tresorier') -def edit_article(request, articleid): +@can_edit(Article) +def edit_article(request, article_instance, articleid): """Edition d'un article (designation, prix, etc) Réservé au trésorier""" - try: - article_instance = Article.objects.get(pk=articleid) - except Article.DoesNotExist: - messages.error(request, u"Entrée inexistante") - return redirect(reverse('cotisations:index-article')) article = ArticleForm(request.POST or None, instance=article_instance) if article.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -397,14 +355,14 @@ def edit_article(request, articleid): ) messages.success(request, "Type d'article modifié") return redirect(reverse('cotisations:index-article')) - return form({'factureform': article}, 'cotisations/facture.html', request) + return form({'factureform': article, 'action_name' : 'Editer'}, 'cotisations/facture.html', request) @login_required -@permission_required('tresorier') -def del_article(request): +@can_delete_set(Article) +def del_article(request, instances): """Suppression d'un article en vente""" - article = DelArticleForm(request.POST or None) + article = DelArticleForm(request.POST or None, instances=instances) if article.is_valid(): article_del = article.cleaned_data['articles'] with transaction.atomic(), reversion.create_revision(): @@ -412,11 +370,11 @@ def del_article(request): reversion.set_user(request.user) messages.success(request, "Le/les articles ont été supprimé") return redirect(reverse('cotisations:index-article')) - return form({'factureform': article}, 'cotisations/facture.html', request) + return form({'factureform': article, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request) @login_required -@permission_required('tresorier') +@can_create(Paiement) def add_paiement(request): """Ajoute un moyen de paiement. Relié aux factures via foreign key""" @@ -428,18 +386,13 @@ def add_paiement(request): reversion.set_comment("Création") messages.success(request, "Le moyen de paiement a été ajouté") return redirect(reverse('cotisations:index-paiement')) - return form({'factureform': paiement}, 'cotisations/facture.html', request) + return form({'factureform': paiement, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request) @login_required -@permission_required('tresorier') -def edit_paiement(request, paiementid): +@can_edit(Paiement) +def edit_paiement(request, paiement_instance, paiementid): """Edition d'un moyen de paiement""" - try: - paiement_instance = Paiement.objects.get(pk=paiementid) - except Paiement.DoesNotExist: - messages.error(request, u"Entrée inexistante") - return redirect(reverse('cotisations:index-paiement')) paiement = PaiementForm(request.POST or None, instance=paiement_instance) if paiement.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -452,14 +405,14 @@ def edit_paiement(request, paiementid): ) messages.success(request, "Type de paiement modifié") return redirect(reverse('cotisations:index-paiement')) - return form({'factureform': paiement}, 'cotisations/facture.html', request) + return form({'factureform': paiement, 'action_name' : 'Editer'}, 'cotisations/facture.html', request) @login_required -@permission_required('tresorier') -def del_paiement(request): +@can_delete_set(Paiement) +def del_paiement(request, instances): """Suppression d'un moyen de paiement""" - paiement = DelPaiementForm(request.POST or None) + paiement = DelPaiementForm(request.POST or None, instances=instances) if paiement.is_valid(): paiement_dels = paiement.cleaned_data['paiements'] for paiement_del in paiement_dels: @@ -479,11 +432,11 @@ def del_paiement(request): facture, vous ne pouvez pas le supprimer" % paiement_del ) return redirect(reverse('cotisations:index-paiement')) - return form({'factureform': paiement}, 'cotisations/facture.html', request) + return form({'factureform': paiement, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request) @login_required -@permission_required('cableur') +@can_create(Banque) def add_banque(request): """Ajoute une banque à la liste des banques""" banque = BanqueForm(request.POST or None) @@ -494,18 +447,13 @@ def add_banque(request): reversion.set_comment("Création") messages.success(request, "La banque a été ajoutée") return redirect(reverse('cotisations:index-banque')) - return form({'factureform': banque}, 'cotisations/facture.html', request) + return form({'factureform': banque, 'action_name' : 'Ajouter'}, 'cotisations/facture.html', request) @login_required -@permission_required('tresorier') -def edit_banque(request, banqueid): +@can_edit(Banque) +def edit_banque(request, banque_instance, banqueid): """Edite le nom d'une banque""" - try: - banque_instance = Banque.objects.get(pk=banqueid) - except Banque.DoesNotExist: - messages.error(request, u"Entrée inexistante") - return redirect(reverse('cotisations:index-banque')) banque = BanqueForm(request.POST or None, instance=banque_instance) if banque.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -518,14 +466,14 @@ def edit_banque(request, banqueid): ) messages.success(request, "Banque modifiée") return redirect(reverse('cotisations:index-banque')) - return form({'factureform': banque}, 'cotisations/facture.html', request) + return form({'factureform': banque, 'action_name' : 'Editer'}, 'cotisations/facture.html', request) @login_required -@permission_required('tresorier') -def del_banque(request): +@can_delete_set(Banque) +def del_banque(request, instances): """Supprime une banque""" - banque = DelBanqueForm(request.POST or None) + banque = DelBanqueForm(request.POST or None, instances=instances) if banque.is_valid(): banque_dels = banque.cleaned_data['banques'] for banque_del in banque_dels: @@ -539,16 +487,16 @@ def del_banque(request): messages.error(request, "La banque %s est affectée à au moins\ une facture, vous ne pouvez pas la supprimer" % banque_del) return redirect(reverse('cotisations:index-banque')) - return form({'factureform': banque}, 'cotisations/facture.html', request) + return form({'factureform': banque, 'action_name' : 'Supprimer'}, 'cotisations/facture.html', request) @login_required -@permission_required('tresorier') +@can_view_all(Facture) +@can_change(Facture, 'control') def control(request): """Pour le trésorier, vue pour controler en masse les factures.Case à cocher, pratique""" - options, _created = GeneralOption.objects.get_or_create() - pagination_number = options.pagination_number + pagination_number = GeneralOption.get_cached_value('pagination_number') facture_list = Facture.objects.select_related('user').select_related('paiement') facture_list = SortTable.sort( facture_list, @@ -583,7 +531,7 @@ def control(request): @login_required -@permission_required('cableur') +@can_view_all(Article) def index_article(request): """Affiche l'ensemble des articles en vente""" article_list = Article.objects.order_by('name') @@ -593,7 +541,7 @@ def index_article(request): @login_required -@permission_required('cableur') +@can_view_all(Paiement) def index_paiement(request): """Affiche l'ensemble des moyens de paiement en vente""" paiement_list = Paiement.objects.order_by('moyen') @@ -603,7 +551,7 @@ def index_paiement(request): @login_required -@permission_required('cableur') +@can_view_all(Banque) def index_banque(request): """Affiche l'ensemble des banques""" banque_list = Banque.objects.order_by('name') @@ -613,11 +561,10 @@ def index_banque(request): @login_required -@permission_required('cableur') +@can_view_all(Facture) def index(request): """Affiche l'ensemble des factures, pour les cableurs et +""" - options, _created = GeneralOption.objects.get_or_create() - pagination_number = options.pagination_number + pagination_number = GeneralOption.get_cached_value('pagination_number') facture_list = Facture.objects.select_related('user')\ .select_related('paiement').prefetch_related('vente_set') facture_list = SortTable.sort( @@ -642,57 +589,122 @@ def index(request): @login_required -def history(request, object_name, object_id): - """Affiche l'historique de chaque objet""" - if object_name == 'facture': - try: - object_instance = Facture.objects.get(pk=object_id) - except Facture.DoesNotExist: - messages.error(request, "Facture inexistante") - return redirect(reverse('cotisations:index')) - if not request.user.has_perms(('cableur',))\ - and object_instance.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher l'historique\ - d'une facture d'un autre user que vous sans droit cableur") +def new_facture_solde(request, userid): + """Creation d'une facture pour un user. Renvoie la liste des articles + et crée des factures dans un formset. Utilise un peu de js coté template + pour ajouter des articles. + Parse les article et boucle dans le formset puis save les ventes, + enfin sauve la facture parente. + TODO : simplifier cette fonction, déplacer l'intelligence coté models + Facture et Vente.""" + user = request.user + facture = Facture(user=user) + paiement, _created = Paiement.objects.get_or_create(moyen='Solde') + facture.paiement = paiement + # Le template a besoin de connaitre les articles pour le js + article_list = Article.objects.filter( + Q(type_user='All') | Q(type_user=request.user.class_name) + ) + if request.user.is_class_club: + article_formset = formset_factory(SelectClubArticleForm)(request.POST or None) + else: + article_formset = formset_factory(SelectUserArticleForm)(request.POST or None) + if article_formset.is_valid(): + articles = article_formset + # Si au moins un article est rempli + if any(art.cleaned_data for art in articles): + user_solde = OptionalUser.get_cached_value('user_solde') + solde_negatif = OptionalUser.get_cached_value('solde_negatif') + # Si on paye par solde, que l'option est activée, + # on vérifie que le négatif n'est pas atteint + if user_solde: + prix_total = 0 + for art_item in articles: + if art_item.cleaned_data: + prix_total += art_item.cleaned_data['article']\ + .prix*art_item.cleaned_data['quantity'] + if float(user.solde) - float(prix_total) < solde_negatif: + messages.error(request, "Le solde est insuffisant pour\ + effectuer l'opération") + return redirect(reverse( + 'users:profil', + kwargs={'userid': userid} + )) + with transaction.atomic(), reversion.create_revision(): + facture.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + for art_item in articles: + if art_item.cleaned_data: + article = art_item.cleaned_data['article'] + quantity = art_item.cleaned_data['quantity'] + new_vente = Vente.objects.create( + facture=facture, + name=article.name, + prix=article.prix, + type_cotisation=article.type_cotisation, + duration=article.duration, + number=quantity + ) + with transaction.atomic(), reversion.create_revision(): + new_vente.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + if any(art_item.cleaned_data['article'].type_cotisation + for art_item in articles if art_item.cleaned_data): + messages.success( + request, + "La cotisation a été prolongée\ + pour l'adhérent %s jusqu'au %s" % ( + user.pseudo, user.end_adhesion() + ) + ) + else: + messages.success(request, "La facture a été crée") return redirect(reverse( 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - elif object_name == 'paiement' and request.user.has_perms(('cableur',)): - try: - object_instance = Paiement.objects.get(pk=object_id) - except Paiement.DoesNotExist: - messages.error(request, "Paiement inexistant") - return redirect(reverse('cotisations:index')) - elif object_name == 'article' and request.user.has_perms(('cableur',)): - try: - object_instance = Article.objects.get(pk=object_id) - except Article.DoesNotExist: - messages.error(request, "Article inexistante") - return redirect(reverse('cotisations:index')) - elif object_name == 'banque' and request.user.has_perms(('cableur',)): - try: - object_instance = Banque.objects.get(pk=object_id) - except Banque.DoesNotExist: - messages.error(request, "Banque inexistante") - return redirect(reverse('cotisations:index')) - else: - messages.error(request, "Objet inconnu") - return redirect(reverse('cotisations:index')) - options, _created = GeneralOption.objects.get_or_create() - pagination_number = options.pagination_number - reversions = Version.objects.get_for_object(object_instance) - paginator = Paginator(reversions, pagination_number) - page = request.GET.get('page') - try: - reversions = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - reversions = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', { - 'reversions': reversions, - 'object': object_instance - }) + kwargs={'userid': userid} + )) + messages.error( + request, + u"Il faut au moins un article valide pour créer une facture" + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': userid} + )) + + return form({ + 'venteform': article_formset, + 'articlelist': article_list + }, 'cotisations/new_facture_solde.html', request) + + +@login_required +def recharge(request): + if AssoOption.get_cached_value('payment') == 'NONE': + messages.error( + request, + "Le paiement en ligne est désactivé." + ) + return redirect(reverse( + 'users:profil', + kwargs={'userid': request.user.id} + )) + f = RechargeForm(request.POST or None, user=request.user) + if f.is_valid(): + facture = Facture(user=request.user) + paiement, _created = Paiement.objects.get_or_create(moyen='Rechargement en ligne') + facture.paiement = paiement + facture.valid = False + facture.save() + v = Vente.objects.create( + facture=facture, + name='solde', + prix=f.cleaned_data['value'], + number=1, + ) + v.save() + content = payment.PAYMENT_SYSTEM[AssoOption.get_cached_value('payment')](facture, request) + return render(request, 'cotisations/payment.html', content) + return form({'rechargeform':f}, 'cotisations/recharge.html', request) diff --git a/install_re2o.sh b/install_re2o.sh index 7f1c46de..80379bdb 100755 --- a/install_re2o.sh +++ b/install_re2o.sh @@ -28,7 +28,7 @@ setup_ldap() { install_re2o_server() { -echo "Installation de Re2o ! +echo "Installation de Re2o ! Cet utilitaire va procéder à l'installation initiale de re2o. Le serveur présent doit être vierge. Preconfiguration..." @@ -36,6 +36,15 @@ export DEBIAN_FRONTEND=noninteractive apt-get -y install sudo dialog +HEIGHT=15 +WIDTH=40 +init=$(dialog --clear \ + --title "Installation de Re2o !" \ + --msgbox "Cet utilitaire va procéder à l'installation initiale de re2o. Le serveur présent doit être vierge de préférence. Preconfiguration..." \ + $HEIGHT $WIDTH \ + 2>&1 >/dev/tty) + + HEIGHT=15 WIDTH=40 CHOICE_HEIGHT=4 @@ -99,7 +108,7 @@ clear if [ $sql_is_local == 2 ] -then +then TITLE="Login sql" sql_login=$(dialog --title "$TITLE" \ --backtitle "$BACKTITLE" \ @@ -145,7 +154,14 @@ ldap_is_local=$(dialog --clear \ "${OPTIONS[@]}" \ 2>&1 >/dev/tty) -echo "Vous devrez fournir un login/host dans le cas où le ldap est non local" + +HEIGHT=15 +WIDTH=40 +instal_ldap=$(dialog --clear \ + --title "Installation de Re2o !" \ + --msgbox "Vous devrez fournir un login/host dans le cas où le ldap est non local" \ + $HEIGHT $WIDTH \ + 2>&1 >/dev/tty) TITLE="Mot de passe ldap" ldap_password=$(dialog --title "$TITLE" \ @@ -154,7 +170,7 @@ ldap_password=$(dialog --title "$TITLE" \ 2>&1 >/dev/tty) clear if [ $ldap_is_local == 2 ] -then +then TITLE="Cn ldap admin" ldap_cn=$(dialog --title "$TITLE" \ --backtitle "$BACKTITLE" \ @@ -194,7 +210,7 @@ email_port=$(dialog --clear \ 2>&1 >/dev/tty) clear if [ $ldap_is_local == 2 ] -then +then TITLE="Cn ldap admin" ldap_cn=$(dialog --title "$TITLE" \ --backtitle "$BACKTITLE" \ @@ -213,9 +229,16 @@ ldap_cn+=$ldap_dn ldap_host="localhost" fi +HEIGHT=15 +WIDTH=40 +install_base=$(dialog --clear \ + --title "Installation de Re2o !" \ + --msgbox "Installation des paquets de base" \ + $HEIGHT $WIDTH \ + 2>&1 >/dev/tty) echo "Installation des paquets de base" -apt-get -y install python3-django python3-dateutil texlive-latex-base texlive-fonts-recommended python3-djangorestframework python3-django-reversion python3-pip libsasl2-dev libldap2-dev libssl-dev +apt-get -y install python3-django python3-dateutil texlive-latex-base texlive-fonts-recommended python3-djangorestframework python3-django-reversion python3-pip libsasl2-dev libldap2-dev libssl-dev python3-crypto pip3 install django-bootstrap3 pip3 install django-ldapdb pip3 install django-macaddress @@ -232,7 +255,7 @@ then echo $mysql_command while true; do read -p "Continue (y/n)?" choice - case "$choice" in + case "$choice" in y|Y ) break;; n|N ) exit;; * ) echo "invalid";; @@ -255,14 +278,14 @@ else echo sudo -u postgres psql $pgsql_command3 while true; do read -p "Continue (y/n)?" choice - case "$choice" in + case "$choice" in y|Y ) break;; n|N ) exit;; * ) echo "invalid";; esac done fi -fi +fi if [ $ldap_is_local == 1 ] then @@ -270,13 +293,20 @@ then setup_ldap $ldap_password $ldap_dn else -echo "Vous devrez manuellement effectuer les opérations de setup de la base ldap sur le serveurs distant. -Lancez la commande : ./install_re2o.sh ldap $ldap_password $ldap_dn" + +HEIGHT=15 +WIDTH=40 +ldap_setup=$(dialog --clear \ + --title "Setup ldap" \ + --msgbox "Vous devrez manuellement effectuer les opérations de setup de la base ldap sur le serveurs distant. Lancez la commande : ./install_re2o.sh ldap $ldap_password $ldap_dn" \ + $HEIGHT $WIDTH \ + 2>&1 >/dev/tty) fi echo "Ecriture de settings_local" django_secret_key=$(python -c "import random; print(''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789%=+') for i in range(50)]))") +aes_key=$(python -c "import random; print(''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789%=+') for i in range(32)]))") cp re2o/settings_local.example.py re2o/settings_local.py if [ $sql_bdd_type == 1 ] @@ -286,6 +316,7 @@ else sed -i 's/db_engine/django.db.backends.postgresql_psycopg2/g' re2o/settings_local.py fi sed -i 's/SUPER_SECRET_KEY/'"$django_secret_key"'/g' re2o/settings_local.py +sed -i 's/THE_AES_KEY/'"$aes_key"'/g' re2o/settings_local.py sed -i 's/SUPER_SECRET_DB/'"$sql_password"'/g' re2o/settings_local.py sed -i 's/db_name_value/'"$sql_name"'/g' re2o/settings_local.py sed -i 's/db_user_value/'"$sql_login"'/g' re2o/settings_local.py @@ -298,10 +329,22 @@ sed -i 's/example.org/'"$extension_locale"'/g' re2o/settings_local.py sed -i 's/MY_EMAIL_HOST/'"$email_host"'/g' re2o/settings_local.py sed -i 's/MY_EMAIL_PORT/'"$email_port"'/g' re2o/settings_local.py -echo "Application des migrations" +HEIGHT=15 +WIDTH=40 +migrations=$(dialog --clear \ + --title "Setup django" \ + --msgbox "Application des migrations" \ + $HEIGHT $WIDTH \ + 2>&1 >/dev/tty) python3 manage.py migrate -echo "Collecte des statics" +HEIGHT=15 +WIDTH=40 +static=$(dialog --clear \ + --title "Setup django" \ + --msgbox "Collecte des statiques" \ + $HEIGHT $WIDTH \ + 2>&1 >/dev/tty) python3 manage.py collectstatic BACKTITLE="Fin de l'installation" @@ -319,7 +362,7 @@ web_serveur=$(dialog --clear \ clear -TITLE="Url où servir le serveur web (ex : re2o.example.org)" +TITLE="Url où servir le serveur web (ex : re2o.example.org). Assurez-vous que ce tld existe bien et répond auprès du DNS" url_server=$(dialog --title "$TITLE" \ --backtitle "$BACKTITLE" \ --inputbox "$TITLE" $HEIGHT $WIDTH \ @@ -365,11 +408,25 @@ sed -i 's|PATH|'"$current_path"'|g' /etc/apache2/sites-available/re2o.conf a2ensite re2o service apache2 reload else -echo "Nginx non supporté, vous devrez installer manuellement" +HEIGHT=15 +WIDTH=40 +web_server=$(dialog --clear \ + --title "Setup serveur web" \ + --msgbox "Nginx non supporté, vous devrez installer manuellement" \ + $HEIGHT $WIDTH \ + 2>&1 >/dev/tty) + fi python3 manage.py createsuperuser +HEIGHT=15 +WIDTH=40 +end=$(dialog --clear \ + --title "Installation terminée" \ + --msgbox "Vous pouvez à présent vous rendre sur $url_server, et vous connecter. Votre utilisateur dispose des privilèges superuser" \ + $HEIGHT $WIDTH \ + 2>&1 >/dev/tty) } main_function() { @@ -377,7 +434,7 @@ if [ ! -z "$1" ] then if [ $1 == ldap ] then -if [ ! -z "$2" ] +if [ ! -z "$2" ] then echo Installation du ldap setup_ldap $2 $3 diff --git a/logs/__init__.py b/logs/__init__.py index fc1be5d7..df6e4256 100644 --- a/logs/__init__.py +++ b/logs/__init__.py @@ -21,3 +21,4 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from .acl import * diff --git a/logs/acl.py b/logs/acl.py new file mode 100644 index 00000000..4a1417f6 --- /dev/null +++ b/logs/acl.py @@ -0,0 +1,40 @@ +# -*- mode: python; coding: utf-8 -*- +# 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. + +"""logs.acl + +Here are defined some functions to check acl on the application. +""" + +def can_view(user): + """Check if an user can view the application. + + Args: + user: The user who wants to view the application. + + Returns: + A couple (allowed, msg) where allowed is a boolean which is True if + viewing is granted and msg is a message (can be None). + """ + can = user.has_module_perms('admin') + return can, None if can else "Vous ne pouvez pas voir cette application." diff --git a/logs/templates/logs/aff_stats_logs.html b/logs/templates/logs/aff_stats_logs.html index 08178a38..77e9e9b4 100644 --- a/logs/templates/logs/aff_stats_logs.html +++ b/logs/templates/logs/aff_stats_logs.html @@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% load logs_extra %} +{% load acl %}
{{ paiement.moyen }} - {% if is_trez %} - - + {% can_edit paiement %} + + - {% endif %} + {% acl_end %} - +
+ + {{ form.instance.user.name }} {{ form.instance.user.surname }}
@@ -47,19 +48,19 @@ with this program; if not, write to the Free Software Foundation, Inc., - {% if is_bureau %} + {% can_edit_history %} - {% endif %} + {% acl_end %} {% endfor %} {% endfor %}
{{ revision.user }} {{ revision.date_created }} {{ revision.comment }} - + Annuler
- + {% if revisions_list.paginator %} {% include "pagination.html" with list=revisions_list %} {% endif %} diff --git a/logs/templates/logs/aff_summary.html b/logs/templates/logs/aff_summary.html index 65f71aca..f743d637 100644 --- a/logs/templates/logs/aff_summary.html +++ b/logs/templates/logs/aff_summary.html @@ -27,7 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% load logs_extra %} - +{% load acl %} @@ -51,14 +51,14 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} ) - {% if is_bureau %} + {% can_edit_history %} - {% endif %} + {% acl_end %} {% elif v.version.content_type.model == 'whitelist' %} @@ -74,14 +74,14 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} ) - {% if is_bureau %} + {% can_edit_history%} - {% endif %} + {% acl_end %} {% elif v.version.content_type.model == 'user' %} @@ -93,14 +93,14 @@ with this program; if not, write to the Free Software Foundation, Inc., ({{ v.comment }}) {% endif %} - {% if is_bureau %} + {% can_edit_history %} - {% endif %} + {% acl_end %} {% elif v.version.content_type.model == 'vente' %} @@ -112,14 +112,14 @@ with this program; if not, write to the Free Software Foundation, Inc., (+{{ v.version.object.duration }} mois) {% endif %} - {% if is_bureau %} + {% can_edit_history %} - {% endif %} + {% acl_end %} {% elif v.version.content_type.model == 'interface' %} @@ -131,14 +131,14 @@ with this program; if not, write to the Free Software Foundation, Inc., ({{ v.comment }}) {% endif %} - {% if is_bureau %} + {% can_edit_history %} - {% endif %} + {% acl_end %} {% endif %} {% endfor %} diff --git a/logs/templates/logs/sidebar.html b/logs/templates/logs/sidebar.html index 4137741f..0e3048e3 100644 --- a/logs/templates/logs/sidebar.html +++ b/logs/templates/logs/sidebar.html @@ -23,32 +23,33 @@ 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 %} {% block sidebar %} - {% if is_cableur %} + {% can_view_app logs %} - + Résumé - + Évènements - + Général - + Base de données - + Actions de cablage - + Utilisateurs - {% endif %} + {% acl_end %} {% endblock %} diff --git a/logs/views.py b/logs/views.py index 1bd91a97..48e52fe0 100644 --- a/logs/views.py +++ b/logs/views.py @@ -41,7 +41,7 @@ from django.urls import reverse from django.shortcuts import render, redirect from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib import messages -from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.decorators import login_required from django.db.models import Count from reversion.models import Revision @@ -50,7 +50,6 @@ from reversion.models import Version, ContentType from users.models import ( User, ServiceUser, - Right, School, ListRight, ListShell, @@ -93,7 +92,17 @@ from topologie.models import ( ) from preferences.models import GeneralOption from re2o.views import form -from re2o.utils import all_whitelisted, all_baned, all_has_access, all_adherent +from re2o.utils import ( + all_whitelisted, + all_baned, + all_has_access, + all_adherent, +) +from re2o.acl import ( + can_view_all, + can_view_app, + can_edit_history, +) from re2o.utils import all_active_assigned_interfaces_count from re2o.utils import all_active_interfaces_count, SortTable @@ -108,12 +117,11 @@ STATS_DICT = { @login_required -@permission_required('cableur') +@can_view_app('logs') def index(request): """Affiche les logs affinés, date reformatées, selectionne les event importants (ajout de droits, ajout de ban/whitelist)""" - options, _created = GeneralOption.objects.get_or_create() - pagination_number = options.pagination_number + pagination_number = GeneralOption.get_cached_value('pagination_number') # The types of content kept for display content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user'] # Select only wanted versions @@ -167,12 +175,11 @@ def index(request): @login_required -@permission_required('cableur') +@can_view_all(GeneralOption) def stats_logs(request): """Affiche l'ensemble des logs et des modifications sur les objets, classés par date croissante, en vrac""" - options, _created = GeneralOption.objects.get_or_create() - pagination_number = options.pagination_number + pagination_number = GeneralOption.get_cached_value('pagination_number') revisions = Revision.objects.all().select_related('user')\ .prefetch_related('version_set__object') revisions = SortTable.sort( @@ -197,7 +204,7 @@ def stats_logs(request): @login_required -@permission_required('bureau') +@can_edit_history def revert_action(request, revision_id): """ Annule l'action en question """ try: @@ -215,7 +222,9 @@ def revert_action(request, revision_id): @login_required -@permission_required('cableur') +@can_view_all(IpList) +@can_view_all(Interface) +@can_view_all(User) def stats_general(request): """Statistiques générales affinées sur les ip, activées, utilisées par range, et les statistiques générales sur les users : users actifs, @@ -298,7 +307,10 @@ def stats_general(request): @login_required -@permission_required('cableur') +@can_view_app('users') +@can_view_app('cotisations') +@can_view_app('machines') +@can_view_app('topologie') def stats_models(request): """Statistiques générales, affiche les comptages par models: nombre d'users, d'écoles, de droits, de bannissements, @@ -310,7 +322,6 @@ def stats_models(request): 'clubs': [Club.PRETTY_NAME, Club.objects.count()], 'serviceuser': [ServiceUser.PRETTY_NAME, ServiceUser.objects.count()], - 'right': [Right.PRETTY_NAME, Right.objects.count()], 'school': [School.PRETTY_NAME, School.objects.count()], 'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()], 'listshell': [ListShell.PRETTY_NAME, ListShell.objects.count()], @@ -340,7 +351,7 @@ def stats_models(request): OuverturePortList.objects.count() ], 'vlan': [Vlan.PRETTY_NAME, Vlan.objects.count()], - 'SOA': [Mx.PRETTY_NAME, Mx.objects.count()], + 'SOA': [SOA.PRETTY_NAME, SOA.objects.count()], 'Mx': [Mx.PRETTY_NAME, Mx.objects.count()], 'Ns': [Ns.PRETTY_NAME, Ns.objects.count()], 'nas': [Nas.PRETTY_NAME, Nas.objects.count()], @@ -368,7 +379,7 @@ def stats_models(request): @login_required -@permission_required('cableur') +@can_view_app('users') def stats_users(request): """Affiche les statistiques base de données aggrégées par user : nombre de machines par user, d'etablissements par user, @@ -395,7 +406,7 @@ def stats_users(request): num=Count('whitelist') ).order_by('-num')[:10], 'Droits': User.objects.annotate( - num=Count('right') + num=Count('groups') ).order_by('-num')[:10], }, 'Etablissement': { @@ -422,7 +433,7 @@ def stats_users(request): @login_required -@permission_required('cableur') +@can_view_app('users') def stats_actions(request): """Vue qui affiche les statistiques de modifications d'objets par utilisateurs. diff --git a/machines/__init__.py b/machines/__init__.py index fc1be5d7..df6e4256 100644 --- a/machines/__init__.py +++ b/machines/__init__.py @@ -21,3 +21,4 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from .acl import * diff --git a/machines/acl.py b/machines/acl.py new file mode 100644 index 00000000..f77a93c7 --- /dev/null +++ b/machines/acl.py @@ -0,0 +1,40 @@ +# -*- mode: python; coding: utf-8 -*- +# 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. + +"""machines.acl + +Here are defined some functions to check acl on the application. +""" + +def can_view(user): + """Check if an user can view the application. + + Args: + user: The user who wants to view the application. + + Returns: + A couple (allowed, msg) where allowed is a boolean which is True if + viewing is granted and msg is a message (can be None). + """ + can = user.has_module_perms('machines') + return can, None if can else "Vous ne pouvez pas voir cette application." diff --git a/machines/forms.py b/machines/forms.py index d6aa5e3e..5dc9dd8b 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -38,6 +38,8 @@ from __future__ import unicode_literals from django.forms import ModelForm, Form from django import forms +from re2o.field_permissions import FieldPermissionFormMixin + from .models import ( Domain, Machine, @@ -55,10 +57,11 @@ from .models import ( Nas, IpType, OuverturePortList, + Ipv6List, ) -class EditMachineForm(ModelForm): +class EditMachineForm(FieldPermissionFormMixin, ModelForm): """Formulaire d'édition d'une machine""" class Meta: model = Machine @@ -76,14 +79,7 @@ class NewMachineForm(EditMachineForm): fields = ['name'] -class BaseEditMachineForm(EditMachineForm): - """Edition basique, ne permet que de changer le nom et le statut. - Réservé aux users sans droits spécifiques""" - class Meta(EditMachineForm.Meta): - fields = ['name', 'active'] - - -class EditInterfaceForm(ModelForm): +class EditInterfaceForm(FieldPermissionFormMixin, ModelForm): """Edition d'une interface. Edition complète""" class Meta: model = Interface @@ -91,16 +87,24 @@ class EditInterfaceForm(ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) + user = kwargs.get('user') super(EditInterfaceForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['mac_address'].label = 'Adresse mac' self.fields['type'].label = 'Type de machine' self.fields['type'].empty_label = "Séléctionner un type de machine" if "ipv4" in self.fields: - self.fields['ipv4'].empty_label = "Assignation automatique\ - de l'ipv4" + self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" self.fields['ipv4'].queryset = IpList.objects.filter( interface__isnull=True ) + if not IpType.can_use_all(user): + self.fields['ipv4'].queryset = IpList.objects.filter( + interface__isnull=True + ).filter(ip_type__in=IpType.objects.filter(need_infra=False)) + else: + self.fields['ipv4'].queryset = IpList.objects.filter( + interface__isnull=True + ) # Add it's own address self.fields['ipv4'].queryset |= IpList.objects.filter( interface=self.instance @@ -108,6 +112,10 @@ class EditInterfaceForm(ModelForm): if "machine" in self.fields: self.fields['machine'].queryset = Machine.objects.all()\ .select_related('user') + if not MachineType.can_use_all(user): + self.fields['type'].queryset = MachineType.objects.filter( + ip_type__in=IpType.objects.filter(need_infra=False) + ) class AddInterfaceForm(EditInterfaceForm): @@ -116,58 +124,6 @@ class AddInterfaceForm(EditInterfaceForm): class Meta(EditInterfaceForm.Meta): fields = ['type', 'ipv4', 'mac_address', 'details'] - def __init__(self, *args, **kwargs): - infra = kwargs.pop('infra') - super(AddInterfaceForm, self).__init__(*args, **kwargs) - self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" - if not infra: - self.fields['type'].queryset = MachineType.objects.filter( - ip_type__in=IpType.objects.filter(need_infra=False) - ) - self.fields['ipv4'].queryset = IpList.objects.filter( - interface__isnull=True - ).filter(ip_type__in=IpType.objects.filter(need_infra=False)) - else: - self.fields['ipv4'].queryset = IpList.objects.filter( - interface__isnull=True - ) - - -class NewInterfaceForm(EditInterfaceForm): - """Formulaire light, sans choix de l'ipv4; d'ajout d'une interface""" - class Meta(EditInterfaceForm.Meta): - fields = ['type', 'mac_address', 'details'] - - -class BaseEditInterfaceForm(EditInterfaceForm): - """Edition basique d'une interface. En fonction des droits, - ajoute ou non l'ensemble des ipv4 disponibles (infra)""" - class Meta(EditInterfaceForm.Meta): - fields = ['type', 'ipv4', 'mac_address', 'details'] - - def __init__(self, *args, **kwargs): - infra = kwargs.pop('infra') - super(BaseEditInterfaceForm, self).__init__(*args, **kwargs) - self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" - if not infra: - self.fields['type'].queryset = MachineType.objects.filter( - ip_type__in=IpType.objects.filter(need_infra=False) - ) - self.fields['ipv4'].queryset = IpList.objects.filter( - interface__isnull=True - ).filter(ip_type__in=IpType.objects.filter(need_infra=False)) - # Add it's own address - self.fields['ipv4'].queryset |= IpList.objects.filter( - interface=self.instance - ) - else: - self.fields['ipv4'].queryset = IpList.objects.filter( - interface__isnull=True - ) - self.fields['ipv4'].queryset |= IpList.objects.filter( - interface=self.instance - ) - class AliasForm(ModelForm): """Ajout d'un alias (et edition), CNAME, contenant nom et extension""" @@ -177,9 +133,10 @@ class AliasForm(ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) - infra = kwargs.pop('infra') + user = kwargs.pop('user') super(AliasForm, self).__init__(*args, prefix=prefix, **kwargs) - if not infra: + can_use_all, reason = Extension.can_use_all(user) + if not can_use_all: self.fields['extension'].queryset = Extension.objects.filter( need_infra=False ) @@ -233,11 +190,19 @@ class MachineTypeForm(ModelForm): class DelMachineTypeForm(Form): """Suppression d'un ou plusieurs machinetype""" machinetypes = forms.ModelMultipleChoiceField( - queryset=MachineType.objects.all(), + queryset=MachineType.objects.none(), label="Types de machines actuelles", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelMachineTypeForm, self).__init__(*args, **kwargs) + if instances: + self.fields['machinetypes'].queryset = instances + else: + self.fields['machinetypes'].queryset = MachineType.objects.all() + class IpTypeForm(ModelForm): """Formulaire d'ajout d'un iptype. Pas d'edition de l'ip de start et de @@ -264,11 +229,19 @@ class EditIpTypeForm(IpTypeForm): class DelIpTypeForm(Form): """Suppression d'un ou plusieurs iptype""" iptypes = forms.ModelMultipleChoiceField( - queryset=IpType.objects.all(), + queryset=IpType.objects.none(), label="Types d'ip actuelles", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelIpTypeForm, self).__init__(*args, **kwargs) + if instances: + self.fields['iptypes'].queryset = instances + else: + self.fields['iptypes'].queryset = IpType.objects.all() + class ExtensionForm(ModelForm): """Formulaire d'ajout et edition d'une extension""" @@ -288,11 +261,30 @@ class ExtensionForm(ModelForm): class DelExtensionForm(Form): """Suppression d'une ou plusieurs extensions""" extensions = forms.ModelMultipleChoiceField( - queryset=Extension.objects.all(), + queryset=Extension.objects.none(), label="Extensions actuelles", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelExtensionForm, self).__init__(*args, **kwargs) + if instances: + self.fields['extensions'].queryset = instances + else: + self.fields['extensions'].queryset = Extension.objects.all() + + +class Ipv6ListForm(FieldPermissionFormMixin, ModelForm): + """Gestion des ipv6 d'une machine""" + class Meta: + model = Ipv6List + fields = ['ipv6', 'slaac_ip'] + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(Ipv6ListForm, self).__init__(*args, prefix=prefix, **kwargs) + class SOAForm(ModelForm): """Ajout et edition d'un SOA""" @@ -308,11 +300,19 @@ class SOAForm(ModelForm): class DelSOAForm(Form): """Suppression d'un ou plusieurs SOA""" soa = forms.ModelMultipleChoiceField( - queryset=SOA.objects.all(), + queryset=SOA.objects.none(), label="SOA actuels", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelSOAForm, self).__init__(*args, **kwargs) + if instances: + self.fields['soa'].queryset = instances + else: + self.fields['soa'].queryset = SOA.objects.all() + class MxForm(ModelForm): """Ajout et edition d'un MX""" @@ -327,15 +327,22 @@ class MxForm(ModelForm): interface_parent=None ).select_related('extension') - class DelMxForm(Form): """Suppression d'un ou plusieurs MX""" mx = forms.ModelMultipleChoiceField( - queryset=Mx.objects.all(), + queryset=Mx.objects.none(), label="MX actuels", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelMxForm, self).__init__(*args, **kwargs) + if instances: + self.fields['mx'].queryset = instances + else: + self.fields['mx'].queryset = Mx.objects.all() + class NsForm(ModelForm): """Ajout d'un NS pour une zone @@ -356,11 +363,19 @@ class NsForm(ModelForm): class DelNsForm(Form): """Suppresion d'un ou plusieurs NS""" ns = forms.ModelMultipleChoiceField( - queryset=Ns.objects.all(), + queryset=Ns.objects.none(), label="Enregistrements NS actuels", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelNsForm, self).__init__(*args, **kwargs) + if instances: + self.fields['ns'].queryset = instances + else: + self.fields['ns'].queryset = Ns.objects.all() + class TxtForm(ModelForm): """Ajout d'un txt pour une zone""" @@ -376,12 +391,20 @@ class TxtForm(ModelForm): class DelTxtForm(Form): """Suppression d'un ou plusieurs TXT""" txt = forms.ModelMultipleChoiceField( - queryset=Txt.objects.all(), + queryset=Txt.objects.none(), label="Enregistrements Txt actuels", widget=forms.CheckboxSelectMultiple ) - + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelTxtForm, self).__init__(*args, **kwargs) + if instances: + self.fields['txt'].queryset = instances + else: + self.fields['txt'].queryset = Txt.objects.all() + + class SrvForm(ModelForm): """Ajout d'un srv pour une zone""" class Meta: @@ -396,11 +419,19 @@ class SrvForm(ModelForm): class DelSrvForm(Form): """Suppression d'un ou plusieurs Srv""" srv = forms.ModelMultipleChoiceField( - queryset=Srv.objects.all(), + queryset=Srv.objects.none(), label="Enregistrements Srv actuels", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelSrvForm, self).__init__(*args, **kwargs) + if instances: + self.fields['srv'].queryset = instances + else: + self.fields['srv'].queryset = Srv.objects.all() + class NasForm(ModelForm): """Ajout d'un type de nas (machine d'authentification, @@ -417,11 +448,19 @@ class NasForm(ModelForm): class DelNasForm(Form): """Suppression d'un ou plusieurs nas""" nas = forms.ModelMultipleChoiceField( - queryset=Nas.objects.all(), + queryset=Nas.objects.none(), label="Enregistrements Nas actuels", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelNasForm, self).__init__(*args, **kwargs) + if instances: + self.fields['nas'].queryset = instances + else: + self.fields['nas'].queryset = Nas.objects.all() + class ServiceForm(ModelForm): """Ajout et edition d'une classe de service : dns, dhcp, etc""" @@ -446,11 +485,19 @@ class ServiceForm(ModelForm): class DelServiceForm(Form): """Suppression d'un ou plusieurs service""" service = forms.ModelMultipleChoiceField( - queryset=Service.objects.all(), + queryset=Service.objects.none(), label="Services actuels", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelServiceForm, self).__init__(*args, **kwargs) + if instances: + self.fields['service'].queryset = instances + else: + self.fields['service'].queryset = Service.objects.all() + class VlanForm(ModelForm): """Ajout d'un vlan : id, nom""" @@ -466,11 +513,19 @@ class VlanForm(ModelForm): class DelVlanForm(Form): """Suppression d'un ou plusieurs vlans""" vlan = forms.ModelMultipleChoiceField( - queryset=Vlan.objects.all(), + queryset=Vlan.objects.none(), label="Vlan actuels", widget=forms.CheckboxSelectMultiple ) + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelVlanForm, self).__init__(*args, **kwargs) + if instances: + self.fields['vlan'].queryset = instances + else: + self.fields['vlan'].queryset = Vlan.objects.all() + class EditOuverturePortConfigForm(ModelForm): """Edition de la liste des profils d'ouverture de ports diff --git a/machines/migrations/0070_auto_20171231_1947.py b/machines/migrations/0070_auto_20171231_1947.py new file mode 100644 index 00000000..ef441d01 --- /dev/null +++ b/machines/migrations/0070_auto_20171231_1947.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-31 18:47 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0069_auto_20171116_0822'), + ] + + operations = [ + migrations.AlterModelOptions( + name='domain', + options={'permissions': (('view_domain', 'Peut voir un objet domain'),)}, + ), + migrations.AlterModelOptions( + name='extension', + options={'permissions': (('view_extension', 'Peut voir un objet extension'), ('use_all_extension', 'Peut utiliser toutes les extension'))}, + ), + migrations.AlterModelOptions( + name='interface', + options={'permissions': (('view_interface', 'Peut voir un objet interface'),)}, + ), + migrations.AlterModelOptions( + name='iplist', + options={'permissions': (('view_iplist', 'Peut voir un objet iplist'),)}, + ), + migrations.AlterModelOptions( + name='iptype', + options={'permissions': (('view_iptype', 'Peut voir un objet iptype'), ('use_all_iptype', 'Peut utiliser tous les iptype'))}, + ), + migrations.AlterModelOptions( + name='machine', + options={'permissions': (('view_machine', 'Peut voir un objet machine quelquonque'), ('change_machine_user', "Peut changer le propriétaire d'une machine"))}, + ), + migrations.AlterModelOptions( + name='machinetype', + options={'permissions': (('view_machinetype', 'Peut voir un objet machinetype'), ('use_all_machinetype', "Peut utiliser n'importe quel type de machine"))}, + ), + migrations.AlterModelOptions( + name='mx', + options={'permissions': (('view_mx', 'Peut voir un objet mx'),)}, + ), + migrations.AlterModelOptions( + name='nas', + options={'permissions': (('view_nas', 'Peut voir un objet Nas'),)}, + ), + migrations.AlterModelOptions( + name='ns', + options={'permissions': (('view_nx', 'Peut voir un objet nx'),)}, + ), + migrations.AlterModelOptions( + name='ouvertureportlist', + options={'permissions': (('view_ouvertureportlist', 'Peut voir un objet ouvertureport'),)}, + ), + migrations.AlterModelOptions( + name='service', + options={'permissions': (('view_service', 'Peut voir un objet service'),)}, + ), + migrations.AlterModelOptions( + name='soa', + options={'permissions': (('view_soa', 'Peut voir un objet soa'),)}, + ), + migrations.AlterModelOptions( + name='srv', + options={'permissions': (('view_soa', 'Peut voir un objet soa'),)}, + ), + migrations.AlterModelOptions( + name='txt', + options={'permissions': (('view_txt', 'Peut voir un objet txt'),)}, + ), + migrations.AlterModelOptions( + name='vlan', + options={'permissions': (('view_vlan', 'Peut voir un objet vlan'),)}, + ), + ] diff --git a/machines/migrations/0071_auto_20171231_2100.py b/machines/migrations/0071_auto_20171231_2100.py new file mode 100644 index 00000000..776edd82 --- /dev/null +++ b/machines/migrations/0071_auto_20171231_2100.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-31 20:00 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0070_auto_20171231_1947'), + ] + + operations = [ + migrations.AlterModelOptions( + name='ns', + options={'permissions': (('view_ns', 'Peut voir un objet ns'),)}, + ), + ] diff --git a/machines/migrations/0072_auto_20180108_1822.py b/machines/migrations/0072_auto_20180108_1822.py new file mode 100644 index 00000000..64999764 --- /dev/null +++ b/machines/migrations/0072_auto_20180108_1822.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-08 17:22 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0071_auto_20171231_2100'), + ] + + operations = [ + migrations.AlterModelOptions( + name='interface', + options={'permissions': (('view_interface', 'Peut voir un objet interface'), ('change_interface_machine', "Peut changer le propriétaire d'une interface"))}, + ), + ] diff --git a/machines/migrations/0073_auto_20180128_2203.py b/machines/migrations/0073_auto_20180128_2203.py new file mode 100644 index 00000000..b09b9c47 --- /dev/null +++ b/machines/migrations/0073_auto_20180128_2203.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-28 21:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0072_auto_20180108_1822'), + ] + + operations = [ + migrations.CreateModel( + name='Ipv6List', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ipv6', models.GenericIPAddressField(protocol='IPv6', unique=True)), + ('slaac_ip', models.BooleanField(default=False)), + ('interface', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='machines.Interface')), + ], + ), + migrations.AlterUniqueTogether( + name='ipv6list', + unique_together=set([('interface', 'slaac_ip')]), + ), + ] diff --git a/machines/migrations/0074_auto_20180129_0352.py b/machines/migrations/0074_auto_20180129_0352.py new file mode 100644 index 00000000..298f2a8e --- /dev/null +++ b/machines/migrations/0074_auto_20180129_0352.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-29 02:52 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0073_auto_20180128_2203'), + ] + + operations = [ + migrations.AlterModelOptions( + name='ipv6list', + options={'permissions': (('view_ipv6list', 'Peut voir un objet ipv6'), ('change_ipv6list_slaac_ip', 'Peut changer la valeur slaac sur une ipv6'))}, + ), + ] diff --git a/machines/migrations/0075_auto_20180130_0052.py b/machines/migrations/0075_auto_20180130_0052.py new file mode 100644 index 00000000..473824b8 --- /dev/null +++ b/machines/migrations/0075_auto_20180130_0052.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-29 23:52 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0074_auto_20180129_0352'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='ipv6list', + unique_together=set([]), + ), + ] diff --git a/machines/migrations/0076_auto_20180130_1623.py b/machines/migrations/0076_auto_20180130_1623.py new file mode 100644 index 00000000..9aab328b --- /dev/null +++ b/machines/migrations/0076_auto_20180130_1623.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-30 15:23 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0075_auto_20180130_0052'), + ] + + operations = [ + migrations.AlterField( + model_name='ipv6list', + name='interface', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ipv6list', to='machines.Interface'), + ), + ] diff --git a/machines/models.py b/machines/models.py index 9bfb4b55..43906bab 100644 --- a/machines/models.py +++ b/machines/models.py @@ -26,6 +26,7 @@ from __future__ import unicode_literals from datetime import timedelta import re from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress +from ipaddress import IPv6Address from django.db import models from django.db.models.signals import post_save, post_delete @@ -37,8 +38,13 @@ from django.core.validators import MaxValueValidator from macaddress.fields import MACAddressField +from re2o.field_permissions import FieldPermissionModelMixin -class Machine(models.Model): +import users.models +import preferences.models + + +class Machine(FieldPermissionModelMixin, models.Model): """ Class définissant une machine, object parent user, objets fils interfaces""" PRETTY_NAME = "Machine" @@ -52,6 +58,105 @@ class Machine(models.Model): ) active = models.BooleanField(default=True) + class Meta: + permissions = ( + ("view_machine", "Peut voir un objet machine quelquonque"), + ("change_machine_user", "Peut changer le propriétaire d'une machine"), + ) + + def get_instance(machineid, *args, **kwargs): + """Récupère une instance + :param machineid: Instance id à trouver + :return: Une instance machine évidemment""" + return Machine.objects.get(pk=machineid) + + @staticmethod + def can_change_user(user_request, *args, **kwargs): + """Checks if an user is allowed to change the user who owns a + Machine. + + Args: + user_request: The user requesting to change owner. + + Returns: + A tuple with a boolean stating if edition is allowed and an + explanation message. + """ + return user_request.has_perm('machines.change_machine_user'), "Vous ne pouvez pas modifier l'utilisateur de la machine." + + def can_create(user_request, userid, *args, **kwargs): + """Vérifie qu'un user qui fait la requète peut bien créer la machine + et n'a pas atteint son quota, et crée bien une machine à lui + :param user_request: Utilisateur qui fait la requête + :param userid: id de l'user dont on va créer une machine + :return: soit True, soit False avec la raison de l'échec""" + try: + user = users.models.User.objects.get(pk=userid) + except users.models.User.DoesNotExist: + return False, u"Utilisateur inexistant" + max_lambdauser_interfaces = preferences.models.OptionalMachine.get_cached_value('max_lambdauser_interfaces') + if not user_request.has_perm('machines.add_machine'): + if not preferences.models.OptionalMachine.get_cached_value('create_machine'): + return False, u"Vous ne pouvez pas ajouter une machine" + if user != user_request: + return False, u"Vous ne pouvez pas ajouter une machine à un\ + autre user que vous sans droit" + if user.user_interfaces().count() >= max_lambdauser_interfaces: + return False, u"Vous avez atteint le maximum d'interfaces\ + autorisées que vous pouvez créer vous même (%s) "\ + % max_lambdauser_interfaces + return True, None + + def can_edit(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien éditer cette instance particulière (soit + machine de soi, soit droit particulier + :param self: instance machine à éditer + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison le cas échéant""" + if self.user != user_request: + if not user_request.has_perm('machines.change_interface') or not self.user.can_edit(self.user, user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer une machine\ + d'un autre user que vous sans droit" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien supprimer cette instance particulière (soit + machine de soi, soit droit particulier + :param self: instance machine à supprimer + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if self.user != user_request: + if not user_request.has_perm('machines.change_interface') or not self.user.can_edit(self.user, user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer une machine\ + d'un autre user que vous sans droit" + return True, None + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des machines, + droit particulier correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_machine'): + return False, u"Vous ne pouvez pas afficher l'ensemble des machines sans permission" + return True, None + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière (soit + machine de soi, soit droit particulier + :param self: instance machine à éditer + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_machine') and self.user != user_request: + return False, u"Vous n'avez pas droit de voir les machines autre\ + que les vôtres" + return True, None + + def __init__(self, *args, **kwargs): + super(Machine, self).__init__(*args, **kwargs) + self.field_permissions = { + 'user' : self.can_change_user, + } + def __str__(self): return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name) @@ -68,11 +173,81 @@ class MachineType(models.Model): null=True ) + class Meta: + permissions = ( + ("view_machinetype", "Peut voir un objet machinetype"), + ("use_all_machinetype", "Peut utiliser n'importe quel type de machine"), + ) + def all_interfaces(self): """ Renvoie toutes les interfaces (cartes réseaux) de type machinetype""" return Interface.objects.filter(type=self) + def get_instance(machinetypeid, *args, **kwargs): + """Récupère une instance + :param machinetypeid: Instance id à trouver + :return: Une instance machinetype évidemment""" + return MachineType.objects.get(pk=machinetypeid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un type de machine + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_machinetype'), u"Vous n'avez pas le droit\ + de créer un type de machine" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance type de machine + :param self: Instance machinetype à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_machinetype'): + return False, u"Vous n'avez pas le droit d'éditer des types de machine" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien supprimer cette instance particulière (soit + machinetype de soi, soit droit particulier + :param self: instance machinetype à supprimer + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.delete_machinetype'): + return False, u"Vous n'avez pas le droit de supprimer des types de machines" + return True, None + + def can_use_all(user_request, *args, **kwargs): + """Check if an user can use every MachineType. + + Args: + user_request: The user requesting edition. + Returns: + A tuple with a boolean stating if user can acces and an explanation + message is acces is not allowed. + """ + if not user_request.has_perm('machines.use_all_machinetype'): + return False, u"Vous n'avez pas le droit d'utiliser tout types de machines" + return True, None + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des machinetype, + droit particulier correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_machinetype'), u"Vous n'avez pas le droit\ + de voir les types de machines" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance machinetype à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_machinetype'), u"Vous n'avez pas le droit\ + de voir les types de machines" + def __str__(self): return self.type @@ -103,6 +278,12 @@ class IpType(models.Model): null=True ) + class Meta: + permissions = ( + ("view_iptype", "Peut voir un objet iptype"), + ("use_all_iptype", "Peut utiliser tous les iptype"), + ) + @cached_property def ip_range(self): """ Renvoie un objet IPRange à partir de l'objet IpType""" @@ -157,6 +338,14 @@ class IpType(models.Model): for ip in self.ip_objects(): ip.delete() + def check_replace_prefixv6(self): + """Remplace les prefixv6 des interfaces liées à ce type d'ip""" + if not self.prefix_v6: + return + else: + for ipv6 in Ipv6List.objects.filter(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): """ Nettoyage. Vérifie : - Que ip_stop est après ip_start @@ -183,6 +372,63 @@ class IpType(models.Model): self.clean() super(IpType, self).save(*args, **kwargs) + def get_instance(iptypeid, *args, **kwargs): + """Récupère une instance + :param iptypeid: Instance id à trouver + :return: Une instance iptype évidemment""" + return IpType.objects.get(pk=iptypeid) + + def can_use_all(user_request, *args, **kwargs): + """Superdroit qui permet d'utiliser toutes les extensions sans restrictions + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.use_all_iptype'), None + + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un type d'ip + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_iptype'), u"Vous n'avez pas le droit\ + de créer un type d'ip" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance iptype + :param self: Instance iptype à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_iptype'): + return False, u"Vous n'avez pas le droit d'éditer des types d'ip" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour supprimer + cette instance iptype + :param self: Instance iptype à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.delete_iptype'), u"Vous n'avez pas le droit\ + de supprimer un type d'ip" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des iptype, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_iptype'), u"Vous n'avez pas le droit\ + de voir les types d'ip" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance iptype à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_iptype'), u"Vous n'avez pas le droit\ + de voir les types d'ip" + def __str__(self): return self.type @@ -196,6 +442,61 @@ class Vlan(models.Model): name = models.CharField(max_length=256) comment = models.CharField(max_length=256, blank=True) + class Meta: + permissions = ( + ("view_vlan", "Peut voir un objet vlan"), + ) + + def get_instance(vlanid, *args, **kwargs): + """Récupère une instance + :param vlanid: Instance id à trouver + :return: Une instance vlan évidemment""" + return Vlan.objects.get(pk=vlanid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un vlan + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_vlan'), u"Vous n'avez pas le droit\ + de créer un vlan" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance vlan + :param self: Instance vlan à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_vlan'): + return False, u"Vous n'avez pas le droit d'éditer des vlans" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour supprimer + cette instance vlan + :param self: Instance vlan à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.delete_vlan'), u"Vous n'avez pas le droit\ + de suprimer un vlan" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des vlan, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_vlan'), u"Vous n'avez pas le droit\ + de voir les vlans" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance vlan à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_vlan'), u"Vous n'avez pas le droit\ + de voir les vlans" + def __str__(self): return self.name @@ -230,6 +531,62 @@ class Nas(models.Model): ) autocapture_mac = models.BooleanField(default=False) + class Meta: + permissions = ( + ("view_nas", "Peut voir un objet Nas"), + ) + + def get_instance(nasid, *args, **kwargs): + """Récupère une instance + :param nasid: Instance id à trouver + :return: Une instance nas évidemment""" + return Nas.objects.get(pk=nasid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un nas + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_nas'), u"Vous n'avez pas le droit\ + de créer un nas" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance nas + :param self: Instance nas à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_nas'): + return False, u"Vous n'avez pas le droit d'éditer des nas" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour supprimer + cette instance nas + :param self: Instance nas à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.delete_nas'), u"Vous n'avez pas le droit\ + de supprimer un nas" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des nas, + droit particulier view objet correspondant + + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_nas'), u"Vous n'avez pas le droit\ + de voir les nas" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance nas à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_nas'), u"Vous n'avez pas le droit\ + de voir les nas" + def __str__(self): return self.name @@ -266,6 +623,61 @@ class SOA(models.Model): help_text='Time To Live' ) + class Meta: + permissions = ( + ("view_soa", "Peut voir un objet soa"), + ) + + def get_instance(soaid, *args, **kwargs): + """Récupère une instance + :param soaid: Instance id à trouver + :return: Une instance soa évidemment""" + return SOA.objects.get(pk=soaid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un soa + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_soa'), u"Vous n'avez pas le droit\ + de créer un enregistrement SOA" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance soa + :param self: Instance soa à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_soa'): + return False, u"Vous n'avez pas le droit d'éditer des enregistrements SOA" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour supprimer + cette instance soa + :param self: Instance soa à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.delete_soa'), u"Vous n'avez pas le droit\ + de supprimer des enregistrements SOA" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des soa, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_soa'), u"Vous n'avez pas le droit\ + de voir les enreistrement SOA" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance soa à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_soa'), u"Vous n'avez pas le droit\ + de voir les enreistrement SOA" + def __str__(self): return str(self.name) @@ -336,6 +748,12 @@ class Extension(models.Model): default=SOA.new_default_soa ) + class Meta: + permissions = ( + ("view_extension", "Peut voir un objet extension"), + ("use_all_extension", "Peut utiliser toutes les extension"), + ) + @cached_property def dns_entry(self): """ Une entrée DNS A et AAAA sur origin (zone self)""" @@ -348,10 +766,66 @@ class Extension(models.Model): entry += "@ IN AAAA " + str(self.origin_v6) return entry + def get_instance(extensionid, *args, **kwargs): + """Récupère une instance + :param extensionid: Instance id à trouver + :return: Une instance extension évidemment""" + return Extension.objects.get(pk=extensionid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + une extension + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_extension'), u"Vous n'avez pas le droit\ + de créer une extension" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance extension + :param self: Instance extension à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_extension'): + return False, u"Vous n'avez pas le droit d'éditer des extensions" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour supprimer + cette instance extension + :param self: Instance extension à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.delete_extension'), u"Vous n'avez pas le droit\ + de supprimer des extension" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des extension, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_extension'), u"Vous n'avez pas le droit\ + de voir les extensions" + + def can_use_all(user_request, *args, **kwargs): + """Superdroit qui permet d'utiliser toutes les extensions sans restrictions + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.use_all_extension'), None + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance extension à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_extension'), u"Vous n'avez pas le droit\ + de voir les extensions" + def __str__(self): return self.name - def clean(self): + def clean(self, *args, **kwargs): if self.name and self.name[0] != '.': raise ValidationError("Une extension doit commencer par un point") super(Extension, self).clean(*args, **kwargs) @@ -367,12 +841,67 @@ class Mx(models.Model): priority = models.PositiveIntegerField(unique=True) name = models.OneToOneField('Domain', on_delete=models.PROTECT) + class Meta: + permissions = ( + ("view_mx", "Peut voir un objet mx"), + ) + @cached_property def dns_entry(self): """Renvoie l'entrée DNS complète pour un MX à mettre dans les fichiers de zones""" return "@ IN MX " + str(self.priority).ljust(3) + " " + str(self.name) + def get_instance(mxid, *args, **kwargs): + """Récupère une instance + :param mxid: Instance id à trouver + :return: Une instance mx évidemment""" + return Mx.objects.get(pk=mxid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un mx + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_mx'), u"Vous n'avez pas le droit\ + de créer un enregistrement MX" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance mx + :param self: Instance mx à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_mx'): + return False, u"Vous n'avez pas le droit d'éditer des enregstrements MX" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour del + cette instance mx + :param self: Instance mx à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.delete_mx'), u"Vous n'avez pas le droit\ + de supprimer un enregistrement MX" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des mx, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_mx'), u"Vous n'avez pas le droit\ + de voir les enregistrements MX" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance mx à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_mx'), u"Vous n'avez pas le droit\ + de voir les enregistrements MX" + def __str__(self): return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name) @@ -384,11 +913,66 @@ class Ns(models.Model): zone = models.ForeignKey('Extension', on_delete=models.PROTECT) ns = models.OneToOneField('Domain', on_delete=models.PROTECT) + class Meta: + permissions = ( + ("view_ns", "Peut voir un objet ns"), + ) + @cached_property def dns_entry(self): """Renvoie un enregistrement NS complet pour les filezones""" return "@ IN NS " + str(self.ns) + def get_instance(nsid, *args, **kwargs): + """Récupère une instance + :param nsid: Instance id à trouver + :return: Une instance ns évidemment""" + return Ns.objects.get(pk=nsid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un ns + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_ns'), u"Vous n'avez pas le droit\ + de créer un enregistrement NS" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance ns + :param self: Instance ns à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_ns'): + return False, u"Vous n'avez pas le droit d'éditer des enregistrements NS" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour del + cette instance ns + :param self: Instance ns à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.del_ns'), u"Vous n'avez pas le droit\ + de supprimer un enregistrement NS" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des ns, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_ns'), u"Vous n'avez pas le droit\ + de voir les enregistrements NS" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance ns à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_ns'), u"Vous n'avez pas le droit\ + de voir les enregistrements NS" + def __str__(self): return str(self.zone) + ' ' + str(self.ns) @@ -401,6 +985,61 @@ class Txt(models.Model): field1 = models.CharField(max_length=255) field2 = models.TextField(max_length=2047) + class Meta: + permissions = ( + ("view_txt", "Peut voir un objet txt"), + ) + + def get_instance(txtid, *args, **kwargs): + """Récupère une instance + :param txtid: Instance id à trouver + :return: Une instance txt évidemment""" + return Txt.objects.get(pk=txtid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un txt + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_txt'), u"Vous n'avez pas le droit\ + de créer un enregistrement TXT" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance txt + :param self: Instance txt à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_txt'): + return False, u"Vous n'avez pas le droit d'éditer des enregistrement TXT" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour del + cette instance txt + :param self: Instance txt à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.delete_txt'), u"Vous n'avez pas le droit\ + de supprimer des enregistrements TXT" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des txt, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_txt'), u"Vous n'avez pas le droit\ + de voir les enregistrements TXT" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance txt à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_txt'), u"Vous n'avez pas le droit\ + de voir les enregistrements TXT" + def __str__(self): return str(self.zone) + " : " + str(self.field1) + " " +\ str(self.field2) @@ -454,6 +1093,61 @@ class Srv(models.Model): help_text="Serveur cible" ) + class Meta: + permissions = ( + ("view_soa", "Peut voir un objet soa"), + ) + + def get_instance(srvid, *args, **kwargs): + """Récupère une instance + :param srvid: Instance id à trouver + :return: Une instance srv évidemment""" + return Srv.objects.get(pk=srvid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un srv + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_soa'), u"Vous n'avez pas le droit\ + de créer un enregistrement SRV" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance srv + :param self: Instance srv à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_soa'): + return False, u"Vous n'avez pas le droit d'éditer des enregistrements SRV" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour del + cette instance srv + :param self: Instance srv à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.delete_soa'), u"Vous n'avez pas le droit\ + de supprimer un enregistrement SRV" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des srv, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_soa'), u"Vous n'avez pas le droit\ + de voir les enregistrements SRV" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance srv à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_soa'), u"Vous n'avez pas le droit\ + de voir les enregistrements SRV" + def __str__(self): return str(self.service) + ' ' + str(self.protocole) + ' ' +\ str(self.extension) + ' ' + str(self.priority) +\ @@ -468,7 +1162,7 @@ class Srv(models.Model): str(self.port) + ' ' + str(self.target) + '.' -class Interface(models.Model): +class Interface(FieldPermissionModelMixin,models.Model): """ Une interface. Objet clef de l'application machine : - une address mac unique. Possibilité de la rendre unique avec le typemachine @@ -490,6 +1184,12 @@ class Interface(models.Model): details = models.CharField(max_length=255, blank=True) port_lists = models.ManyToManyField('OuverturePortList', blank=True) + class Meta: + permissions = ( + ("view_interface", "Peut voir un objet interface"), + ("change_interface_machine", "Peut changer le propriétaire d'une interface"), + ) + @cached_property def is_active(self): """ Renvoie si une interface doit avoir accès ou non """ @@ -498,7 +1198,7 @@ class Interface(models.Model): return machine.active and user.has_access() @cached_property - def ipv6_object(self): + def ipv6_slaac(self): """ Renvoie un objet type ipv6 à partir du prefix associé à l'iptype parent""" if self.type.ip_type.prefix_v6: @@ -509,9 +1209,58 @@ class Interface(models.Model): return None @cached_property + def gen_ipv6_dhcpv6(self): + """Cree une ip, à assigner avec dhcpv6 sur une machine""" + prefix_v6 = self.type.ip_type.prefix_v6 + if not prefix_v6: + return None + return IPv6Address(IPv6Address(prefix_v6).exploded[:20] + IPv6Address(self.id).exploded[20:]) + + def sync_ipv6_dhcpv6(self): + """Affecte une ipv6 dhcpv6 calculée à partir de l'id de la machine""" + ipv6_dhcpv6 = self.gen_ipv6_dhcpv6 + if not ipv6_dhcpv6: + return + ipv6 = Ipv6List.objects.filter(ipv6=str(ipv6_dhcpv6)).first() + if not ipv6: + ipv6 = Ipv6List(ipv6=str(ipv6_dhcpv6)) + ipv6.interface = self + ipv6.save() + return + + def sync_ipv6_slaac(self): + """Cree, mets à jour et supprime si il y a lieu l'ipv6 slaac associée + à la machine + Sans prefixe ipv6, on return + Si l'ip slaac n'est pas celle qu'elle devrait être, on maj""" + ipv6_slaac = self.ipv6_slaac + if not ipv6_slaac: + return + ipv6_object = Ipv6List.objects.filter(interface=self, slaac_ip=True).first() + if not ipv6_object: + ipv6_object = Ipv6List(interface=self, slaac_ip=True) + if ipv6_object.ipv6 != str(ipv6_slaac): + ipv6_object.ipv6 = str(ipv6_slaac) + ipv6_object.save() + + def sync_ipv6(self): + """Cree et met à jour l'ensemble des ipv6 en fonction du mode choisi""" + if preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'SLAAC': + self.sync_ipv6_slaac() + elif preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'DHCPV6': + self.sync_ipv6_dhcpv6() + else: + return + def ipv6(self): - """ Renvoie l'ipv6 en str. Mise en cache et propriété de l'objet""" - return str(self.ipv6_object) + """ Renvoie le queryset de la liste des ipv6 + On renvoie l'ipv6 slaac que si le mode slaac est activé (et non dhcpv6)""" + if preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'SLAAC': + return self.ipv6list.all() + elif preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'DHCPV6': + return self.ipv6list.filter(slaac_ip=False) + else: + return None def mac_bare(self): """ Formatage de la mac type mac_bare""" @@ -571,6 +1320,90 @@ class Interface(models.Model): correspondent pas") super(Interface, self).save(*args, **kwargs) + def get_instance(interfaceid, *args, **kwargs): + """Récupère une instance + :param interfaceid: Instance id à trouver + :return: Une instance interface évidemment""" + return Interface.objects.get(pk=interfaceid) + + def can_create(user_request, machineid, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + une interface, ou bien que la machine appartient bien à l'user + :param macineid: Id de la machine parente de l'interface + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + try: + machine = Machine.objects.get(pk=machineid) + except Machine.DoesNotExist: + return False, u"Machine inexistante" + if not user_request.has_perm('machines.add_interface'): + if not preferences.models.OptionalMachine.get_cached_value('create_machine'): + return False, u"Vous ne pouvez pas ajouter une machine" + max_lambdauser_interfaces = preferences.models.OptionalMachine.get_cached_value('max_lambdauser_interfaces') + if machine.user != user_request: + return False, u"Vous ne pouvez pas ajouter une interface à une\ + machine d'un autre user que vous sans droit" + if machine.user.user_interfaces().count() >= max_lambdauser_interfaces: + return False, u"Vous avez atteint le maximum d'interfaces\ + autorisées que vous pouvez créer vous même (%s) "\ + % max_lambdauser_interfaces + return True, None + + @staticmethod + def can_change_machine(user_request, *args, **kwargs): + return user_request.has_perm('machines.change_interface_machine'), "Droit requis pour changer la machine" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance interface, ou qu'elle lui appartient + :param self: Instance interface à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if self.machine.user != user_request: + if not user_request.has_perm('machines.change_interface') or not self.machine.user.can_edit(user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer une machine\ + d'un autre user que vous sans droit" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits delete object pour del + cette instance interface, ou qu'elle lui appartient + :param self: Instance interface à del + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if self.machine.user != user_request: + if not user_request.has_perm('machines.change_interface') or not self.machine.user.can_edit(user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer une machine\ + d'un autre user que vous sans droit" + return True, None + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des interfaces, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_interface'): + return False, u"Vous n'avez pas le droit de voir des machines autre\ + que les vôtres" + return True, None + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet ou qu'elle appartient à l'user + :param self: instance interface à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_interface') and self.machine.user != user_request: + return False, u"Vous n'avez pas le droit de voir des machines autre\ + que les vôtres" + return True, None + + def __init__(self, *args, **kwargs): + super(Interface, self).__init__(*args, **kwargs) + self.field_permissions = { + 'machine' : self.can_change_machine, + } + def __str__(self): try: domain = self.domain @@ -592,6 +1425,126 @@ class Interface(models.Model): return self.ipv4 and not self.has_private_ip() +class Ipv6List(FieldPermissionModelMixin, models.Model): + PRETTY_NAME = 'Enregistrements Ipv6 des machines' + + ipv6 = models.GenericIPAddressField( + protocol='IPv6', + unique=True + ) + interface = models.ForeignKey('Interface', on_delete=models.CASCADE, related_name='ipv6list') + slaac_ip = models.BooleanField(default=False) + + class Meta: + permissions = ( + ("view_ipv6list", "Peut voir un objet ipv6"), + ("change_ipv6list_slaac_ip", "Peut changer la valeur slaac sur une ipv6"), + ) + + def get_instance(ipv6listid, *args, **kwargs): + """Récupère une instance + :param interfaceid: Instance id à trouver + :return: Une instance interface évidemment""" + return Ipv6List.objects.get(pk=ipv6listid) + + def can_create(user_request, interfaceid, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + une ipv6, ou possède l'interface associée + :param interfaceid: Id de l'interface associée à cet objet domain + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + try: + interface = Interface.objects.get(pk=interfaceid) + except Interface.DoesNotExist: + return False, u"Interface inexistante" + if not user_request.has_perm('machines.add_ipv6list'): + if interface.machine.user != user_request: + return False, u"Vous ne pouvez pas ajouter un alias à une\ + machine d'un autre user que vous sans droit" + return True, None + + @staticmethod + def can_change_slaac_ip(user_request, *args, **kwargs): + return user_request.has_perm('machines.change_ipv6list_slaac_ip'), "Droit requis pour changer la valeur slaac ip" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance interface, ou qu'elle lui appartient + :param self: Instance interface à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if self.interface.machine.user != user_request: + if not user_request.has_perm('machines.change_ipv6list') or not self.interface.machine.user.can_edit(user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer une machine\ + d'un autre user que vous sans droit" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits delete object pour del + cette instance interface, ou qu'elle lui appartient + :param self: Instance interface à del + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if self.interface.machine.user != user_request: + if not user_request.has_perm('machines.change_ipv6list') or not self.interface.machine.user.can_edit(user_request, *args, **kwargs)[0]: + return False, u"Vous ne pouvez pas éditer une machine\ + d'un autre user que vous sans droit" + return True, None + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des interfaces, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_ipv6list'): + return False, u"Vous n'avez pas le droit de voir des machines autre\ + que les vôtres" + return True, None + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet ou qu'elle appartient à l'user + :param self: instance interface à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_ipv6list') and self.interface.machine.user != user_request: + return False, u"Vous n'avez pas le droit de voir des machines autre\ + que les vôtres" + return True, None + + def __init__(self, *args, **kwargs): + super(Ipv6List, self).__init__(*args, **kwargs) + self.field_permissions = { + 'slaac_ip' : self.can_change_slaac_ip, + } + + def check_and_replace_prefix(self, prefix=None): + """Si le prefixe v6 est incorrect, on maj l'ipv6""" + prefix_v6 = prefix or self.interface.type.ip_type.prefix_v6 + if not prefix_v6: + return + if IPv6Address(self.ipv6).exploded[:20] != IPv6Address(prefix_v6).exploded[:20]: + self.ipv6 = IPv6Address(IPv6Address(prefix_v6).exploded[:20] + IPv6Address(self.ipv6).exploded[20:]) + self.save() + + def clean(self, *args, **kwargs): + if self.slaac_ip and Ipv6List.objects.filter(interface=self.interface, slaac_ip=True).exclude(id=self.id): + raise ValidationError("Une ip slaac est déjà enregistrée") + prefix_v6 = self.interface.type.ip_type.prefix_v6 + if prefix_v6: + if IPv6Address(self.ipv6).exploded[:20] != IPv6Address(prefix_v6).exploded[:20]: + raise ValidationError("Le prefixv6 est incorrect et ne correspond pas au type associé à la machine") + super(Ipv6List, self).clean(*args, **kwargs) + + def save(self, *args, **kwargs): + """Force à avoir appellé clean avant""" + self.full_clean() + super(Ipv6List, self).save(*args, **kwargs) + + def __str__(self): + return str(self.ipv6) + + class Domain(models.Model): """ Objet domain. Enregistrement A et CNAME en même temps : permet de stocker les alias et les nom de machines, suivant si interface_parent @@ -618,6 +1571,9 @@ class Domain(models.Model): class Meta: unique_together = (("name", "extension"),) + permissions = ( + ("view_domain", "Peut voir un objet domain"), + ) def get_extension(self): """ Retourne l'extension de l'interface parente si c'est un A @@ -670,6 +1626,95 @@ class Domain(models.Model): self.full_clean() super(Domain, self).save(*args, **kwargs) + @cached_property + def get_source_interface(self): + """Renvoie l'interface source : + - l'interface reliée si c'est un A + - si c'est un cname, suit le cname jusqu'à atteindre le A + et renvoie l'interface parente + Fonction récursive""" + if self.interface_parent: + return self.interface_parent + else: + return self.cname.get_parent_interface() + + def get_instance(domainid, *args, **kwargs): + """Récupère une instance + :param domainid: Instance id à trouver + :return: Une instance domain évidemment""" + return Domain.objects.get(pk=domainid) + + def can_create(user_request, interfaceid, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un domain, ou possède l'interface associée + :param interfaceid: Id de l'interface associée à cet objet domain + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + try: + interface = Interface.objects.get(pk=interfaceid) + except Interface.DoesNotExist: + return False, u"Interface inexistante" + if not user_request.has_perm('machines.add_domain'): + max_lambdauser_aliases = preferences.models.OptionalMachine.get_cached_value('max_lambdauser_aliases') + if interface.machine.user != user_request: + 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: + return False, u"Vous avez atteint le maximum d'alias\ + autorisés que vous pouvez créer vous même (%s) "\ + % max_lambdauser_aliases + return True, None + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits pour editer + cette instance domain + :param self: Instance domain à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_domain') and\ + self.get_source_interface.machine.user != user_request: + return False, u"Vous ne pouvez pas editer un alias à une machine\ + d'un autre user que vous sans droit" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits delete object pour del + cette instance domain, ou qu'elle lui appartient + :param self: Instance domain à del + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.delete_domain') and\ + self.get_source_interface.machine.user != user_request: + return False, u"Vous ne pouvez pas supprimer un alias à une machine\ + d'un autre user que vous sans droit" + return True, None + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des domain, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_domain'): + return False, u"Vous ne pouvez pas supprimer un alias à une machine\ + d'un autre user que vous sans droit" + return True, None + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet ou qu'elle appartient à l'user + :param self: instance domain à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_domain') and\ + self.get_source_interface.machine.user != user_request: + return False, u"Vous n'avez pas le droit de voir des machines autre\ + que les vôtres" + return True, None + def __str__(self): return str(self.name) + str(self.extension) @@ -680,6 +1725,11 @@ class IpList(models.Model): ipv4 = models.GenericIPAddressField(protocol='IPv4', unique=True) ip_type = models.ForeignKey('IpType', on_delete=models.CASCADE) + class Meta: + permissions = ( + ("view_iplist", "Peut voir un objet iplist"), + ) + @cached_property def need_infra(self): """ Permet de savoir si un user basique peut assigner cette ip ou @@ -697,6 +1747,59 @@ class IpList(models.Model): self.clean() super(IpList, self).save(*args, **kwargs) + def get_instance(iplistid, *args, **kwargs): + """Récupère une instance + :param iplistid: Instance id à trouver + :return: Une instance iplist évidemment""" + return IpList.objects.get(pk=iplistid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + une ip + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_iplist'), u"Vous n'avez pas le droit\ + de créer une ip" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance ip + :param self: Instance ip à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_iplist'): + return False, u"Vous n'avez pas le droit d'éditer des enregistrements ip" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour delete + cette instance ip + :param self: Instance ip à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.delete_iplist'): + return False, u"Vous n'avez pas le droit d'éditer des enregistrements ip" + return True, None + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des ip, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_iplist'): + return False, u"Vous n'avez pas le droit de voir des enregistrements ip" + return True, None + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit infra + :param self: instance iplist à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_iplist'): + return False, u"Vous n'avez pas le droit de voir des enregistrements ip" + return True, None + def __str__(self): return self.ipv4 @@ -716,6 +1819,11 @@ class Service(models.Model): ) servers = models.ManyToManyField('Interface', through='Service_link') + class Meta: + permissions = ( + ("view_service", "Peut voir un objet service"), + ) + def ask_regen(self): """ Marque à True la demande de régénération pour un service x """ Service_link.objects.filter(service=self).exclude(asked_regen=True)\ @@ -737,6 +1845,56 @@ class Service(models.Model): def save(self, *args, **kwargs): super(Service, self).save(*args, **kwargs) + def get_instance(serviceid, *args, **kwargs): + """Récupère une instance + :param serviceid: Instance id à trouver + :return: Une instance service évidemment""" + return Service.objects.get(pk=serviceid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un service + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_service'), u"Vous n'avez pas le droit\ + de créer un service" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance service + :param self: Instance service à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_service'): + return False, u"Vous n'avez pas le droit d'éditer des services" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour delete + cette instance service + :param self: Instance service à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.delete_service'), u"Vous n'avez pas le droit\ + de supprimer un service" + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des services, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_service'), u"Vous n'avez pas le droit\ + de voir des services" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance service à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_service'), u"Vous n'avez pas le droit\ + de voir des services" + def __str__(self): return str(self.service_type) @@ -777,6 +1935,57 @@ class Service_link(models.Model): ) < timezone.now() ) + def get_instance(servicelinkid, *args, **kwargs): + """Récupère une instance + :param servicelinkid: Instance id à trouver + :return: Une instance servicelink évidemment""" + return ServiceLink.objects.get(pk=servicelinkid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour créer + un servicelink + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_service'), u"Vous n'avez pas le droit\ + de créer un service" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour editer + cette instance servicelink + :param self: Instance servicelink à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_service'): + return False, u"Vous n'avez pas le droit d'éditer des services" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits infra pour delete + cette instance servicelink + :param self: Instance servicelink à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.delete_service'): + return False, u"Vous n'avez pas le droit d'éditer des services" + return True, None + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des services, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_service'), u"Vous n'avez pas le droit\ + de voir des liens de services" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance service à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_service'), u"Vous n'avez pas le droit\ + de voir des liens de services" + def __str__(self): return str(self.server) + " " + str(self.service) @@ -790,6 +1999,65 @@ class OuverturePortList(models.Model): max_length=255 ) + class Meta: + permissions = ( + ("view_ouvertureportlist", "Peut voir un objet ouvertureport"), + ) + + def get_instance(ouvertureportlistid, *args, **kwargs): + """Récupère une instance + :param ouvertureportlistid: Instance id à trouver + :return: Une instance ouvertureportlist évidemment""" + return OuverturePortList.objects.get(pk=ouvertureportlistid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits bureau pour créer + une ouverture de port + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_ouvertureportlist') , u"Vous n'avez pas le droit\ + d'ouvrir un port" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits bureau pour editer + cette instance ouvertureportlist + :param self: Instance ouvertureportlist à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_ouvertureportlist'): + return False, u"Vous n'avez pas le droit d'éditer des ouvertures de port" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits bureau pour delete + cette instance ouvertureportlist + :param self: Instance ouvertureportlist à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.delete_ouvertureportlist'): + return False, u"Vous n'avez pas le droit de supprimer une ouverture\ + de port" + if self.interface_set.all(): + return False, u"Cette liste de ports est utilisée" + return True, None + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des ouvertureport, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_ouvertureportlist'), u"Vous n'avez pas le droit\ + de voir des ouverture de ports" + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance ouvertureport à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + return user_request.has_perm('machines.view_ouvertureportlist'), u"Vous n'avez pas le droit\ + de voir des ouverture de ports" + def __str__(self): return self.name @@ -860,6 +2128,60 @@ class OuverturePort(models.Model): default=OUT, ) + def get_instance(ouvertureportid, *args, **kwargs): + """Récupère une instance + :param ouvertureportid: Instance id à trouver + :return: Une instance ouvertureport évidemment""" + return OuverturePort.objects.get(pk=ouvertureportid) + + def can_create(user_request, *args, **kwargs): + """Verifie que l'user a les bons droits bureau pour créer + une ouverture de port + :param user_request: instance utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + return user_request.has_perm('machines.add_ouvertureportlist') , u"Vous n'avez pas le droit\ + d'ouvrir un port" + + def can_edit(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits bureau pour editer + cette instance ouvertureport + :param self: Instance ouvertureport à editer + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.change_ouvertureportlist'): + return False, u"Vous n'avez pas le droit d'éditer des ouvertures de port" + return True, None + + def can_delete(self, user_request, *args, **kwargs): + """Verifie que l'user a les bons droits bureau pour delete + cette instance ouvertureport + :param self: Instance ouvertureport à delete + :param user_request: Utilisateur qui fait la requête + :return: soit True, soit False avec la raison de l'échec""" + if not user_request.has_perm('machines.delete_ouvertureportlist'): + return False, u"Vous n'avez pas le droit d'éditer des ouvertures de port" + return True, None + + def can_view_all(user_request, *args, **kwargs): + """Vérifie qu'on peut bien afficher l'ensemble des ouvertureport, + droit particulier view objet correspondant + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_ouvertureportlist'): + return False, u"Vous n'avez pas le droit d'éditer des ouvertures de port" + return True, None + + def can_view(self, user_request, *args, **kwargs): + """Vérifie qu'on peut bien voir cette instance particulière avec + droit view objet + :param self: instance ouvertureport à voir + :param user_request: instance user qui fait l'edition + :return: True ou False avec la raison de l'échec le cas échéant""" + if not user_request.has_perm('machines.view_ouvertureportlist'): + return False, u"Vous n'avez pas le droit d'éditer des ouvertures de port" + return True, None + + def __str__(self): if self.begin == self.end: return str(self.begin) @@ -896,6 +2218,7 @@ def interface_post_save(sender, **kwargs): """Synchronisation ldap et régen parefeu/dhcp lors de la modification d'une interface""" interface = kwargs['instance'] + interface.sync_ipv6() user = interface.machine.user user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) # Regen services @@ -917,6 +2240,7 @@ def iptype_post_save(sender, **kwargs): """Generation des objets ip après modification d'un range ip""" iptype = kwargs['instance'] iptype.gen_ip_range() + iptype.check_replace_prefixv6() @receiver(post_save, sender=MachineType) @@ -1010,3 +2334,4 @@ def srv_post_save(sender, **kwargs): def text_post_delete(sender, **kwargs): """Regeneration dns après modification d'un SRV""" regen('dns') + diff --git a/machines/serializers.py b/machines/serializers.py index d1876e06..f1888750 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -37,7 +37,8 @@ from machines.models import ( Service_link, Ns, OuverturePortList, - OuverturePort + OuverturePort, + Ipv6List ) @@ -57,6 +58,12 @@ class IpListSerializer(serializers.ModelSerializer): fields = ('ipv4', 'ip_type') +class Ipv6ListSerializer(serializers.ModelSerializer): + class Meta: + model = Ipv6List + fields = ('ipv6', 'slaac_ip') + + class InterfaceSerializer(serializers.ModelSerializer): """Serialisation d'une interface, ipv4, domain et extension sont des foreign_key, on les override et on les evalue avec des fonctions @@ -81,8 +88,9 @@ class InterfaceSerializer(serializers.ModelSerializer): class FullInterfaceSerializer(serializers.ModelSerializer): - """Serialisation complete d'une interface avec l'ipv6 en plus""" + """Serialisation complete d'une interface avec les ipv6 en plus""" ipv4 = IpListSerializer(read_only=True) + ipv6 = Ipv6ListSerializer(read_only=True, many=True) mac_address = serializers.SerializerMethodField('get_macaddress') domain = serializers.SerializerMethodField('get_dns') extension = serializers.SerializerMethodField('get_interface_extension') @@ -124,6 +132,7 @@ class TypeSerializer(serializers.ModelSerializer): class Meta: model = IpType fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop', + 'prefix_v6', 'ouverture_ports_tcp_in', 'ouverture_ports_tcp_out', 'ouverture_ports_udp_in', 'ouverture_ports_udp_out',) diff --git a/machines/templates/machines/aff_alias.html b/machines/templates/machines/aff_alias.html index bd64c737..fb3f0486 100644 --- a/machines/templates/machines/aff_alias.html +++ b/machines/templates/machines/aff_alias.html @@ -22,6 +22,8 @@ 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 %} +
- + Annuler
- + Annuler
- + Annuler
- + Annuler
- + Annuler
@@ -33,7 +35,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html index 15a4c637..0da5a08e 100644 --- a/machines/templates/machines/aff_extension.html +++ b/machines/templates/machines/aff_extension.html @@ -22,6 +22,8 @@ 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 %} +
{{ alias }} + {% can_edit alias %} {% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='alias' id=alias.id %}
@@ -45,9 +47,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} diff --git a/machines/templates/machines/aff_iptype.html b/machines/templates/machines/aff_iptype.html index 454b169d..c5ed4c10 100644 --- a/machines/templates/machines/aff_iptype.html +++ b/machines/templates/machines/aff_iptype.html @@ -22,6 +22,8 @@ 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 %} +
{{ extension.origin_v6 }} - {% if is_infra %} + {% can_create Extension %} {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='extension' id=extension.id %}
@@ -48,9 +50,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_ipv6.html b/machines/templates/machines/aff_ipv6.html new file mode 100644 index 00000000..b6efa3f0 --- /dev/null +++ b/machines/templates/machines/aff_ipv6.html @@ -0,0 +1,51 @@ +{% 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 %} + +
{{ type.vlan }} {{ type.ouverture_ports }} - {% if is_infra %} + {% can_edit type %} {% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='iptype' id=type.id %}
+ + + + + + + + {% for ipv6 in ipv6_list %} + + + + + + {% endfor %} +
Ipv6Slaac
{{ ipv6.ipv6 }}{{ ipv6.slaac_ip }} + {% can_edit ipv6 %} + {% include 'buttons/edit.html' with href='machines:edit-ipv6list' id=ipv6.id %} + {% acl_end %} + {% can_delete ipv6 %} + {% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %} + {% acl_end %} + {% include 'buttons/history.html' with href='machines:history' name='ipv6list' id=ipv6.id %} +
+ diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index 63d35241..c286fcd6 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -22,11 +22,13 @@ 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 %} + {% if machines_list.paginator %} {% include "pagination.html" with list=machines_list %} {% endif %} - +
@@ -44,36 +46,29 @@ with this program; if not, write to the Free Software Foundation, Inc., {% for machine in machines_list %} {% for interface in machine.interface_set.all %} + + {% if ipv6_enabled and interface.ipv6 != 'None'%} + + + + {% endif %} + + + {% if interface.domain.related_domain.all %} + + + + {% endif %} {% endfor %} @@ -126,6 +175,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- {{ machine.name|default:'Pas de nom' }} + {{ machine.name|default:'Pas de nom' }} - {{ machine.user }} + {{ machine.user }} + {% can_create Interface machine.id %} {% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='machine' id=machine.id %} + {% can_delete machine %} {% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %} + {% acl_end %}
{% if interface.domain.related_domain.all %} - + {{ interface.domain }} + {% else %} {{ interface.domain }} {% endif %} @@ -88,36 +83,90 @@ with this program; if not, write to the Free Software Foundation, Inc., IPv4 {{ interface.ipv4 }}
{% if ipv6_enabled and interface.ipv6 != 'None'%} - IPv6 {{ interface.ipv6 }} + IPv6 + {% endif %}
+
+
    + {% for ipv6 in interface.ipv6.all %} +
  • + {{ipv6}} +
  • + {% endfor %} +
+
+
+
+
    + {% for al in interface.domain.related_domain.all %} +
  • + + {{ al }} + + +
  • + {% endfor %} +
+
+
+ + {% if machines_list.paginator %} {% include "pagination.html" with list=machines_list %} {% endif %} diff --git a/machines/templates/machines/aff_machinetype.html b/machines/templates/machines/aff_machinetype.html index 7fdcbdb4..facad203 100644 --- a/machines/templates/machines/aff_machinetype.html +++ b/machines/templates/machines/aff_machinetype.html @@ -22,6 +22,8 @@ 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 %} + @@ -35,9 +37,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_mx.html b/machines/templates/machines/aff_mx.html index 176ab061..4478cdab 100644 --- a/machines/templates/machines/aff_mx.html +++ b/machines/templates/machines/aff_mx.html @@ -22,6 +22,8 @@ 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 %} +
{{ type.type }} {{ type.ip_type }} - {% if is_infra %} + {% can_edit type %} {% include 'buttons/edit.html' with href='machines:edit-machinetype' id=type.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='machinetype' id=type.id %}
@@ -38,9 +40,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_nas.html b/machines/templates/machines/aff_nas.html index dc8fd079..735a4ca8 100644 --- a/machines/templates/machines/aff_nas.html +++ b/machines/templates/machines/aff_nas.html @@ -22,6 +22,8 @@ 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 %} +
{{ mx.priority }} {{ mx.name }} - {% if is_infra %} + {% can_edit mx %} {% include 'buttons/edit.html' with href='machines:edit-mx' id=mx.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='mx' id=mx.id %}
@@ -41,9 +43,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_ns.html b/machines/templates/machines/aff_ns.html index 67a0dd81..5ee87304 100644 --- a/machines/templates/machines/aff_ns.html +++ b/machines/templates/machines/aff_ns.html @@ -22,6 +22,8 @@ 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 %} +
{{ nas.port_access_mode }} {{ nas.autocapture_mac }} - {% if is_infra %} + {% can_edit nas %} {% include 'buttons/edit.html' with href='machines:edit-nas' id=nas.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='nas' id=nas.id %}
@@ -36,9 +38,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_service.html b/machines/templates/machines/aff_service.html index 47bfee25..da80b4da 100644 --- a/machines/templates/machines/aff_service.html +++ b/machines/templates/machines/aff_service.html @@ -22,6 +22,8 @@ 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 %} +
{{ ns.zone }} {{ ns.ns }} - {% if is_infra %} + {% can_edit ns %} {% include 'buttons/edit.html' with href='machines:edit-ns' id=ns.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='ns' id=ns.id %}
@@ -40,9 +42,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_soa.html b/machines/templates/machines/aff_soa.html index 3dad11c7..5352a739 100644 --- a/machines/templates/machines/aff_soa.html +++ b/machines/templates/machines/aff_soa.html @@ -22,6 +22,8 @@ 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 %} +
{{ service.regular_time_regen }} {% for serv in service.servers.all %}{{ serv }}, {% endfor %} - {% if is_infra %} + {% can_edit service %} {% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='service' id=service.id %}
@@ -44,9 +46,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_srv.html b/machines/templates/machines/aff_srv.html index 773815d9..e7886cf1 100644 --- a/machines/templates/machines/aff_srv.html +++ b/machines/templates/machines/aff_srv.html @@ -22,6 +22,8 @@ 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 %} +
{{ soa.expire }} {{ soa.ttl }} - {% if is_infra %} + {% can_edit soa %} {% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='soa' id=soa.id %}
@@ -48,9 +50,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_txt.html b/machines/templates/machines/aff_txt.html index fd7c5ee6..973fd6d9 100644 --- a/machines/templates/machines/aff_txt.html +++ b/machines/templates/machines/aff_txt.html @@ -22,6 +22,8 @@ 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 %} +
{{ srv.port }} {{ srv.target }} - {% if is_infra %} + {% can_edit srv %} {% include 'buttons/edit.html' with href='machines:edit-srv' id=srv.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='srv' id=srv.id %}
@@ -36,9 +38,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/aff_vlan.html b/machines/templates/machines/aff_vlan.html index eaa1c82c..deb8cb11 100644 --- a/machines/templates/machines/aff_vlan.html +++ b/machines/templates/machines/aff_vlan.html @@ -22,6 +22,8 @@ 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 %} +
{{ txt.zone }} {{ txt.dns_entry }} - {% if is_infra %} + {% can_edit txt %} {% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='txt' id=txt.id %}
@@ -39,9 +41,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/machines/templates/machines/index_alias.html b/machines/templates/machines/index_alias.html index 7e9522e6..07750754 100644 --- a/machines/templates/machines/index_alias.html +++ b/machines/templates/machines/index_alias.html @@ -29,8 +29,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

Liste des alias de l'interface

- Ajouter un alias - Supprimer un ou plusieurs alias + Ajouter un alias + Supprimer un ou plusieurs alias {% include "machines/aff_alias.html" with alias_list=alias_list %}

diff --git a/machines/templates/machines/index_extension.html b/machines/templates/machines/index_extension.html index 3169b4c9..6ee1bdff 100644 --- a/machines/templates/machines/index_extension.html +++ b/machines/templates/machines/index_extension.html @@ -25,45 +25,47 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} +{% load acl %} + {% block title %}Machines{% endblock %} {% block content %}

Liste des extensions

- {% if is_infra %} - Ajouter une extension - Supprimer une ou plusieurs extensions - {% endif %} + {% can_create Extension %} + Ajouter une extension + {% acl_end %} + Supprimer une ou plusieurs extensions {% include "machines/aff_extension.html" with extension_list=extension_list %}

Liste des enregistrements SOA

- {% if is_infra %} - Ajouter un enregistrement SOA - Supprimer un enregistrement SOA - {% endif %} + {% can_create SOA %} + Ajouter un enregistrement SOA + {% acl_end %} + Supprimer un enregistrement SOA {% include "machines/aff_soa.html" with soa_list=soa_list %}

Liste des enregistrements MX

- {% if is_infra %} - Ajouter un enregistrement MX - Supprimer un enregistrement MX - {% endif %} + {% can_create Mx %} + Ajouter un enregistrement MX + {% acl_end %} + Supprimer un enregistrement MX {% include "machines/aff_mx.html" with mx_list=mx_list %}

Liste des enregistrements NS

- {% if is_infra %} - Ajouter un enregistrement NS - Supprimer un enregistrement NS - {% endif %} + {% can_create Ns %} + Ajouter un enregistrement NS + {% acl_end %} + Supprimer un enregistrement NS {% include "machines/aff_ns.html" with ns_list=ns_list %}

Liste des enregistrements TXT

- {% if is_infra %} - Ajouter un enregistrement TXT - Supprimer un enregistrement TXT - {% endif %} + {% can_create Txt %} + Ajouter un enregistrement TXT + {% acl_end %} + Supprimer un enregistrement TXT {% include "machines/aff_txt.html" with txt_list=txt_list %}

Liste des enregistrements SRV

- {% if is_infra %} - Ajouter un enregistrement SRV - Supprimer un enregistrement SRV - {% endif %} + {% can_create Srv %} + Ajouter un enregistrement SRV + {% acl_end %} + Supprimer un enregistrement SRV {% include "machines/aff_srv.html" with srv_list=srv_list %}

diff --git a/machines/templates/machines/index_iptype.html b/machines/templates/machines/index_iptype.html index 8f85130b..4dacf96e 100644 --- a/machines/templates/machines/index_iptype.html +++ b/machines/templates/machines/index_iptype.html @@ -25,14 +25,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} +{% load acl %} + {% block title %}Ip{% endblock %} {% block content %}

Liste des types d'ip

- {% if is_infra %} - Ajouter un type d'ip - Supprimer un ou plusieurs types d'ip - {% endif %} + {% can_create IpType %} + Ajouter un type d'ip + {% acl_end %} + Supprimer un ou plusieurs types d'ip {% include "machines/aff_iptype.html" with iptype_list=iptype_list %}

diff --git a/machines/templates/machines/index_ipv6.html b/machines/templates/machines/index_ipv6.html new file mode 100644 index 00000000..584dc00a --- /dev/null +++ b/machines/templates/machines/index_ipv6.html @@ -0,0 +1,41 @@ +{% 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 %} + +{% block title %}Machines{% endblock %} + +{% block content %} +

Liste des ipv6 de l'interface

+ {% can_create Ipv6List interface_id %} + Ajouter une ipv6 + {% acl_end %} + {% include "machines/aff_ipv6.html" with ipv6_list=ipv6_list %} +
+
+
+{% endblock %} + diff --git a/machines/templates/machines/index_machinetype.html b/machines/templates/machines/index_machinetype.html index 1e99fd2d..a0bfd7e5 100644 --- a/machines/templates/machines/index_machinetype.html +++ b/machines/templates/machines/index_machinetype.html @@ -25,14 +25,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} +{% load acl %} + {% block title %}Machines{% endblock %} {% block content %}

Liste des types de machines

- {% if is_infra %} - Ajouter un type de machine - Supprimer un ou plusieurs types de machines - {% endif %} + {% can_create MachineType %} + Ajouter un type de machine + {% acl_end %} + Supprimer un ou plusieurs types de machines {% include "machines/aff_machinetype.html" with machinetype_list=machinetype_list %}

diff --git a/machines/templates/machines/index_nas.html b/machines/templates/machines/index_nas.html index ac1fe8b4..3f1cb90f 100644 --- a/machines/templates/machines/index_nas.html +++ b/machines/templates/machines/index_nas.html @@ -25,16 +25,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} +{% load acl %} + {% block title %}Machines{% endblock %} {% block content %}

Liste des nas

La correpondance nas-machinetype relie le type de nas à un type de machine. Elle est utile pour l'autoenregistrement des macs par radius, et permet de choisir le type de machine à affecter aux machines en fonction du type de nas
- {% if is_infra %} - Ajouter un type de nas - Supprimer un ou plusieurs types nas - {% endif %} + {% can_create Nas %} + Ajouter un type de nas + {% acl_end %} + Supprimer un ou plusieurs types nas {% include "machines/aff_nas.html" with nas_list=nas_list %}

diff --git a/machines/templates/machines/index_portlist.html b/machines/templates/machines/index_portlist.html index 04ac66d4..e505ad43 100644 --- a/machines/templates/machines/index_portlist.html +++ b/machines/templates/machines/index_portlist.html @@ -2,11 +2,15 @@ {% load bootstrap3 %} +{% load acl %} + {% block title %}Configuration de ports{% endblock %} {% block content %}

Liste des configurations de ports

- Ajouter une configuration + {% can_create OuverturePortList %} + Ajouter une configuration + {% acl_end %}
{{ vlan.comment }} {% for range in vlan.iptype_set.all %}{{ range }}, {% endfor%} - {% if is_infra %} + {% can_create Vlan %} {% include 'buttons/edit.html' with href='machines:edit-vlan' id=vlan.id %} - {% endif %} + {% acl_end %} {% include 'buttons/history.html' with href='machines:history' name='vlan' id=vlan.id %}
@@ -44,8 +48,12 @@ {% endif %} {%endfor%} @@ -53,5 +61,5 @@


- + {% endblock %} diff --git a/machines/templates/machines/index_service.html b/machines/templates/machines/index_service.html index b07f994c..3bc88189 100644 --- a/machines/templates/machines/index_service.html +++ b/machines/templates/machines/index_service.html @@ -24,15 +24,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load acl %} {% block title %}Machines{% endblock %} {% block content %}

Liste des services

- {% if is_infra %} - Ajouter un service - Supprimer un ou plusieurs service - {% endif %} + {% can_create machines.Service %} + Ajouter un service + {% acl_end %} + Supprimer un ou plusieurs service {% include "machines/aff_service.html" with service_list=service_list %}

Etat des serveurs

{% include "machines/aff_servers.html" with servers_list=servers_list %} diff --git a/machines/templates/machines/index_vlan.html b/machines/templates/machines/index_vlan.html index ec00b0bf..beb6c80e 100644 --- a/machines/templates/machines/index_vlan.html +++ b/machines/templates/machines/index_vlan.html @@ -25,14 +25,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} +{% load acl %} + {% block title %}Machines{% endblock %} {% block content %}

Liste des vlans

- {% if is_infra %} - Ajouter un vlan - Supprimer un ou plusieurs vlan - {% endif %} + {% can_create Vlan %} + Ajouter un vlan + {% acl_end %} + Supprimer un ou plusieurs vlan {% include "machines/aff_vlan.html" with vlan_list=vlan_list %}

diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 9159a31d..0e7082f8 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -72,12 +72,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if nasform %} {% bootstrap_form_errors nasform %} {% endif %} +{% if ipv6form %} + {% bootstrap_form_errors ipv6form %} +{% endif %} {% csrf_token %} {% if machineform %}

Machine

- {% bootstrap_form machineform %} + {% massive_bootstrap_form machineform 'user' %} {% endif %} {% if interfaceform %}

Interface

@@ -139,7 +142,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,

NAS

{% bootstrap_form nasform %} {% endif %} - {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} + {% if ipv6form %} +

Ipv6

+ {% bootstrap_form ipv6form %} + {% endif %} + {% bootstrap_button action_name button_type="submit" icon="star" %}

diff --git a/machines/templates/machines/sidebar.html b/machines/templates/machines/sidebar.html index 6ca3a07f..5a0f975d 100644 --- a/machines/templates/machines/sidebar.html +++ b/machines/templates/machines/sidebar.html @@ -23,42 +23,55 @@ 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 %} {% block sidebar %} - {% if is_cableur %} + {% can_view_all Machine %} - + Machines + {% acl_end %} + {% can_view_all MachineType %} - + Types de machines + {% acl_end %} + {% can_view_all Extension %} - + Extensions et zones + {% acl_end %} + {% can_view_all IpType %} - + Plages d'IP + {% acl_end %} + {% can_view_all Vlan %} - + Vlans + {% acl_end %} + {% can_view_all Nas %} - + Gestion des nas + {% acl_end %} + {% can_view_all machines.Service %} - + Services (dhcp, dns...) - - {% endif %} - {% if is_cableur %} + + {% acl_end %} + {% can_view_all OuverturePortList %} - + Ouverture de ports - - {%endif%} + + {% acl_end %} {% endblock %} diff --git a/machines/urls.py b/machines/urls.py index c024cf56..e3454097 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -24,7 +24,7 @@ from __future__ import unicode_literals from django.conf.urls import url - +import re2o from . import views urlpatterns = [ @@ -61,9 +61,13 @@ urlpatterns = [ url(r'^del_srv/$', views.del_srv, name='del-srv'), url(r'^index_extension/$', views.index_extension, name='index-extension'), url(r'^add_alias/(?P[0-9]+)$', views.add_alias, name='add-alias'), - url(r'^edit_alias/(?P[0-9]+)$', views.edit_alias, name='edit-alias'), + url(r'^edit_alias/(?P[0-9]+)$', views.edit_alias, name='edit-alias'), url(r'^del_alias/(?P[0-9]+)$', views.del_alias, name='del-alias'), url(r'^index_alias/(?P[0-9]+)$', views.index_alias, name='index-alias'), + url(r'^new_ipv6list/(?P[0-9]+)$', views.new_ipv6list, name='new-ipv6list'), + url(r'^edit_ipv6list/(?P[0-9]+)$', views.edit_ipv6list, name='edit-ipv6list'), + url(r'^del_ipv6list/(?P[0-9]+)$', views.del_ipv6list, name='del-ipv6list'), + url(r'^index_ipv6/(?P[0-9]+)$', views.index_ipv6, name='index-ipv6'), url(r'^add_service/$', views.add_service, name='add-service'), url(r'^edit_service/(?P[0-9]+)$', views.edit_service, name='edit-service'), url(r'^del_service/$', views.del_service, name='del-service'), @@ -76,20 +80,12 @@ urlpatterns = [ url(r'^edit_nas/(?P[0-9]+)$', views.edit_nas, name='edit-nas'), url(r'^del_nas/$', views.del_nas, name='del-nas'), url(r'^index_nas/$', views.index_nas, name='index-nas'), - url(r'^history/(?Pmachine)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pinterface)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pmachinetype)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pextension)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Psoa)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pmx)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pns)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Ptxt)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Psrv)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Piptype)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Palias)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pvlan)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pnas)/(?P[0-9]+)$', views.history, name='history'), - url(r'^history/(?Pservice)/(?P[0-9]+)$', views.history, name='history'), + url( + r'history/(?P\w+)/(?P[0-9]+)$', + re2o.views.history, + name='history', + kwargs={'application':'machines'}, + ), url(r'^$', views.index, name='index'), url(r'^rest/mac-ip/$', views.mac_ip, name='mac-ip'), url(r'^rest/regen-achieved/$', views.regen_achieved, name='regen-achieved'), @@ -104,9 +100,9 @@ urlpatterns = [ url(r'^rest/service_servers/$', views.service_servers, name='service-servers'), url(r'^rest/ouverture_ports/$', views.ouverture_ports, name='ouverture-ports'), url(r'index_portlist/$', views.index_portlist, name='index-portlist'), - url(r'^edit_portlist/(?P[0-9]+)$', views.edit_portlist, name='edit-portlist'), - url(r'^del_portlist/(?P[0-9]+)$', views.del_portlist, name='del-portlist'), + url(r'^edit_portlist/(?P[0-9]+)$', views.edit_portlist, name='edit-portlist'), + url(r'^del_portlist/(?P[0-9]+)$', views.del_portlist, name='del-portlist'), url(r'^add_portlist/$', views.add_portlist, name='add-portlist'), - url(r'^port_config/(?P[0-9]+)$', views.configure_ports, name='port-config'), + url(r'^port_config/(?P[0-9]+)$', views.configure_ports, name='port-config'), ] diff --git a/machines/views.py b/machines/views.py index a59e493c..eb7086c4 100644 --- a/machines/views.py +++ b/machines/views.py @@ -69,8 +69,6 @@ from .forms import ( DelMachineTypeForm, ExtensionForm, DelExtensionForm, - BaseEditInterfaceForm, - BaseEditMachineForm ) from .forms import ( EditIpTypeForm, @@ -95,6 +93,7 @@ from .forms import ( DelNasForm, SrvForm, DelSrvForm, + Ipv6ListForm, ) from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm from .models import ( @@ -116,6 +115,7 @@ from .models import ( Srv, OuverturePortList, OuverturePort, + Ipv6List, ) from users.models import User from preferences.models import GeneralOption, OptionalMachine @@ -123,7 +123,15 @@ from re2o.utils import ( all_active_assigned_interfaces, all_has_access, filter_active_interfaces, - SortTable + SortTable, +) +from re2o.acl import ( + can_create, + can_edit, + can_delete, + can_view, + can_view_all, + can_delete_set, ) from re2o.views import form @@ -210,34 +218,18 @@ def generate_ipv4_mbf_param( form, is_type_tt ): return i_mbf_param @login_required -def new_machine(request, userid): - """ Fonction de creation d'une machine. Cree l'objet machine, +@can_create(Machine) +@can_edit(User) +def new_machine(request, user, userid): + """ Fonction de creation d'une machine. Cree l'objet machine, le sous objet interface et l'objet domain à partir de model forms. Trop complexe, devrait être simplifié""" - try: - user = User.objects.get(pk=userid) - except User.DoesNotExist: - messages.error(request, u"Utilisateur inexistant" ) - return redirect(reverse('machines:index')) - options, created = OptionalMachine.objects.get_or_create() - max_lambdauser_interfaces = options.max_lambdauser_interfaces - if not request.user.has_perms(('cableur',)): - if user != request.user: - messages.error( - request, - "Vous ne pouvez pas ajouter une machine à un autre user que vous sans droit") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - if user.user_interfaces().count() >= max_lambdauser_interfaces: - messages.error(request, "Vous avez atteint le maximum d'interfaces autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_interfaces) - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - machine = NewMachineForm(request.POST or None) - interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) + + machine = NewMachineForm(request.POST or None, user=request.user) + interface = AddInterfaceForm( + request.POST or None, + user=request.user + ) domain = DomainForm(request.POST or None, user=user) if machine.is_valid() and interface.is_valid(): new_machine = machine.save(commit=False) @@ -264,32 +256,33 @@ def new_machine(request, userid): return redirect(reverse( 'users:profil', kwargs={'userid':str(user.id)} - )) - i_mbf_param = generate_ipv4_mbf_param( interface, False ) - return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) + )) + i_mbf_param = generate_ipv4_mbf_param(interface, False) + return form( + { + 'machineform': machine, + 'interfaceform': interface, + 'domainform': domain, + 'i_mbf_param': i_mbf_param, + 'action_name' : 'Créer une machine' + }, + 'machines/machine.html', + request + ) @login_required -def edit_interface(request, interfaceid): +@can_edit(Interface) +def edit_interface(request, interface_instance, interfaceid): """ Edition d'une interface. Distingue suivant les droits les valeurs de interfaces et machines que l'user peut modifier infra permet de modifier le propriétaire""" - try: - interface = Interface.objects.get(pk=interfaceid) - except Interface.DoesNotExist: - messages.error(request, u"Interface inexistante" ) - return redirect(reverse('machines:index')) - if not request.user.has_perms(('infra',)): - if not request.user.has_perms(('cableur',)) and interface.machine.user != request.user: - messages.error(request, "Vous ne pouvez pas éditer une machine d'un autre user que vous sans droit") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - machine_form = BaseEditMachineForm(request.POST or None, instance=interface.machine) - interface_form = BaseEditInterfaceForm(request.POST or None, instance=interface, infra=False) - else: - machine_form = EditMachineForm(request.POST or None, instance=interface.machine) - interface_form = EditInterfaceForm(request.POST or None, instance=interface) - domain_form = DomainForm(request.POST or None, instance=interface.domain) + + machine_form = EditMachineForm( + request.POST or None, + instance=interface_instance.machine, + user=request.user + ) + interface_form = EditInterfaceForm(request.POST or None, instance=interface_instance, user=request.user) + domain_form = DomainForm(request.POST or None, instance=interface_instance.domain) if machine_form.is_valid() and interface_form.is_valid() and domain_form.is_valid(): new_machine = machine_form.save(commit=False) new_interface = interface_form.save(commit=False) @@ -309,26 +302,21 @@ def edit_interface(request, interfaceid): messages.success(request, "La machine a été modifiée") return redirect(reverse( 'users:profil', - kwargs={'userid':str(interface.machine.user.id)} + kwargs={'userid':str(interface_instance.machine.user.id)} )) i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) - return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) + return form({ + 'machineform': machine_form, + 'interfaceform': interface_form, + 'domainform': domain_form, + 'i_mbf_param': i_mbf_param, + 'action_name' : 'Editer une interface' + }, 'machines/machine.html', request) @login_required -def del_machine(request, machineid): +@can_delete(Machine) +def del_machine(request, machine, machineid): """ Supprime une machine, interfaces en mode cascade""" - try: - machine = Machine.objects.get(pk=machineid) - except Machine.DoesNotExist: - messages.error(request, u"Machine inexistante" ) - return redirect(reverse('machines:index')) - if not request.user.has_perms(('cableur',)): - if machine.user != request.user: - messages.error(request, "Vous ne pouvez pas éditer une machine d'un autre user que vous sans droit") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(machine.user.id)} - )) if request.method == "POST": with transaction.atomic(), reversion.create_revision(): machine.delete() @@ -341,29 +329,12 @@ def del_machine(request, machineid): return form({'objet': machine, 'objet_name': 'machine'}, 'machines/delete.html', request) @login_required -def new_interface(request, machineid): +@can_create(Interface) +@can_edit(Machine) +def new_interface(request, machine, machineid): """ Ajoute une interface et son domain associé à une machine existante""" - try: - machine = Machine.objects.get(pk=machineid) - except Machine.DoesNotExist: - messages.error(request, u"Machine inexistante" ) - return redirect(reverse('machines:index')) - if not request.user.has_perms(('cableur',)): - options, created = OptionalMachine.objects.get_or_create() - max_lambdauser_interfaces = options.max_lambdauser_interfaces - if machine.user != request.user: - messages.error(request, "Vous ne pouvez pas ajouter une interface à une machine d'un autre user que vous sans droit") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - if machine.user.user_interfaces().count() >= max_lambdauser_interfaces: - messages.error(request, "Vous avez atteint le maximum d'interfaces autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_interfaces) - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - interface_form = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) + + interface_form = AddInterfaceForm(request.POST or None, user=request.user) domain_form = DomainForm(request.POST or None) if interface_form.is_valid(): new_interface = interface_form.save(commit=False) @@ -386,23 +357,17 @@ def new_interface(request, machineid): kwargs={'userid':str(machine.user.id)} )) i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) - return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) + return form({ + 'interfaceform': interface_form, + 'domainform': domain_form, + 'i_mbf_param': i_mbf_param, + 'action_name' : 'Créer une interface' + }, 'machines/machine.html', request) @login_required -def del_interface(request, interfaceid): +@can_delete(Interface) +def del_interface(request, interface, interfaceid): """ Supprime une interface. Domain objet en mode cascade""" - try: - interface = Interface.objects.get(pk=interfaceid) - except Interface.DoesNotExist: - messages.error(request, u"Interface inexistante" ) - return redirect(reverse('machines:index')) - if not request.user.has_perms(('cableur',)): - if interface.machine.user != request.user: - messages.error(request, "Vous ne pouvez pas éditer une machine d'un autre user que vous sans droit") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) if request.method == "POST": machine = interface.machine with transaction.atomic(), reversion.create_revision(): @@ -418,9 +383,62 @@ def del_interface(request, interfaceid): return form({'objet': interface, 'objet_name': 'interface'}, 'machines/delete.html', request) @login_required -@permission_required('infra') +@can_create(Ipv6List) +@can_edit(Interface) +def new_ipv6list(request, interface, interfaceid): + """Nouvelle ipv6""" + ipv6list_instance = Ipv6List(interface=interface) + ipv6 = Ipv6ListForm(request.POST or None, instance=ipv6list_instance, user=request.user) + if ipv6.is_valid(): + with transaction.atomic(), reversion.create_revision(): + ipv6.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Ipv6 ajoutée") + return redirect(reverse( + 'machines:index-ipv6', + kwargs={'interfaceid':str(interface.id)} + )) + return form({'ipv6form': ipv6, 'action_name' : 'Créer'}, 'machines/machine.html', request) + +@login_required +@can_edit(Ipv6List) +def edit_ipv6list(request, ipv6list_instance, ipv6listid): + """Edition d'une ipv6""" + ipv6 = Ipv6ListForm(request.POST or None, instance=ipv6list_instance, user=request.user) + if ipv6.is_valid(): + with transaction.atomic(), reversion.create_revision(): + ipv6.save() + reversion.set_user(request.user) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ipv6.changed_data)) + messages.success(request, "Ipv6 modifiée") + return redirect(reverse( + 'machines:index-ipv6', + kwargs={'interfaceid':str(ipv6list_instance.interface.id)} + )) + return form({'ipv6form': ipv6, 'action_name' : 'Editer'}, 'machines/machine.html', request) + +@login_required +@can_delete(Ipv6List) +def del_ipv6list(request, ipv6list, ipv6listid): + """ Supprime une ipv6""" + if request.method == "POST": + interfaceid = ipv6list.interface.id + with transaction.atomic(), reversion.create_revision(): + ipv6list.delete() + reversion.set_user(request.user) + messages.success(request, "L'ipv6 a été détruite") + return redirect(reverse( + 'machines:index-ipv6', + kwargs={'interfaceid':str(interfaceid)} + )) + return form({'objet': ipv6list, 'objet_name': 'ipv6'}, 'machines/delete.html', request) + +@login_required +@can_create(IpType) def add_iptype(request): """ Ajoute un range d'ip. Intelligence dans le models, fonction views minimaliste""" + iptype = IpTypeForm(request.POST or None) if iptype.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -429,17 +447,13 @@ def add_iptype(request): reversion.set_comment("Création") messages.success(request, "Ce type d'ip a été ajouté") return redirect(reverse('machines:index-iptype')) - return form({'iptypeform': iptype}, 'machines/machine.html', request) + return form({'iptypeform': iptype, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_iptype(request, iptypeid): +@can_edit(IpType) +def edit_iptype(request, iptype_instance, iptypeid): """ Edition d'un range. Ne permet pas de le redimensionner pour éviter l'incohérence""" - try: - iptype_instance = IpType.objects.get(pk=iptypeid) - except IpType.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-iptype')) + iptype = EditIpTypeForm(request.POST or None, instance=iptype_instance) if iptype.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -448,13 +462,13 @@ def edit_iptype(request, iptypeid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in iptype.changed_data)) messages.success(request, "Type d'ip modifié") return redirect(reverse('machines:index-iptype')) - return form({'iptypeform': iptype}, 'machines/machine.html', request) + return form({'iptypeform': iptype, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_iptype(request): +@can_delete_set(IpType) +def del_iptype(request, instances): """ Suppression d'un range ip. Supprime les objets ip associés""" - iptype = DelIpTypeForm(request.POST or None) + iptype = DelIpTypeForm(request.POST or None, instances=instances) if iptype.is_valid(): iptype_dels = iptype.cleaned_data['iptypes'] for iptype_del in iptype_dels: @@ -466,11 +480,12 @@ def del_iptype(request): except ProtectedError: messages.error(request, "Le type d'ip %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % iptype_del) return redirect(reverse('machines:index-iptype')) - return form({'iptypeform': iptype}, 'machines/machine.html', request) + return form({'iptypeform': iptype, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(MachineType) def add_machinetype(request): + machinetype = MachineTypeForm(request.POST or None) if machinetype.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -479,16 +494,12 @@ def add_machinetype(request): reversion.set_comment("Création") messages.success(request, "Ce type de machine a été ajouté") return redirect(reverse('machines:index-machinetype')) - return form({'machinetypeform': machinetype}, 'machines/machine.html', request) + return form({'machinetypeform': machinetype, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_machinetype(request, machinetypeid): - try: - machinetype_instance = MachineType.objects.get(pk=machinetypeid) - except MachineType.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-machinetype')) +@can_edit(MachineType) +def edit_machinetype(request, machinetype_instance, machinetypeid): + machinetype = MachineTypeForm(request.POST or None, instance=machinetype_instance) if machinetype.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -497,12 +508,12 @@ def edit_machinetype(request, machinetypeid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machinetype.changed_data)) messages.success(request, "Type de machine modifié") return redirect(reverse('machines:index-machinetype')) - return form({'machinetypeform': machinetype}, 'machines/machine.html', request) + return form({'machinetypeform': machinetype, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_machinetype(request): - machinetype = DelMachineTypeForm(request.POST or None) +@can_delete_set(MachineType) +def del_machinetype(request, instances): + machinetype = DelMachineTypeForm(request.POST or None, instances=instances) if machinetype.is_valid(): machinetype_dels = machinetype.cleaned_data['machinetypes'] for machinetype_del in machinetype_dels: @@ -514,11 +525,12 @@ def del_machinetype(request): except ProtectedError: messages.error(request, "Le type de machine %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % machinetype_del) return redirect(reverse('machines:index-machinetype')) - return form({'machinetypeform': machinetype}, 'machines/machine.html', request) + return form({'machinetypeform': machinetype, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(Extension) def add_extension(request): + extension = ExtensionForm(request.POST or None) if extension.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -527,16 +539,12 @@ def add_extension(request): reversion.set_comment("Création") messages.success(request, "Cette extension a été ajoutée") return redirect(reverse('machines:index-extension')) - return form({'extensionform': extension}, 'machines/machine.html', request) + return form({'extensionform': extension, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_extension(request, extensionid): - try: - extension_instance = Extension.objects.get(pk=extensionid) - except Extension.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-extension')) +@can_edit(Extension) +def edit_extension(request, extension_instance, extensionid): + extension = ExtensionForm(request.POST or None, instance=extension_instance) if extension.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -545,12 +553,12 @@ def edit_extension(request, extensionid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in extension.changed_data)) messages.success(request, "Extension modifiée") return redirect(reverse('machines:index-extension')) - return form({'extensionform': extension}, 'machines/machine.html', request) + return form({'extensionform': extension, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_extension(request): - extension = DelExtensionForm(request.POST or None) +@can_delete_set(Extension) +def del_extension(request, instances): + extension = DelExtensionForm(request.POST or None, instances=instances) if extension.is_valid(): extension_dels = extension.cleaned_data['extensions'] for extension_del in extension_dels: @@ -562,11 +570,12 @@ def del_extension(request): except ProtectedError: messages.error(request, "L'extension %s est affectée à au moins un type de machine, vous ne pouvez pas la supprimer" % extension_del) return redirect(reverse('machines:index-extension')) - return form({'extensionform': extension}, 'machines/machine.html', request) + return form({'extensionform': extension, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(SOA) def add_soa(request): + soa = SOAForm(request.POST or None) if soa.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -575,16 +584,12 @@ def add_soa(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement SOA a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'soaform': soa}, 'machines/machine.html', request) + return form({'soaform': soa, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_soa(request, soaid): - try: - soa_instance = SOA.objects.get(pk=soaid) - except SOA.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-extension')) +@can_edit(SOA) +def edit_soa(request, soa_instance, soaid): + soa = SOAForm(request.POST or None, instance=soa_instance) if soa.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -593,12 +598,12 @@ def edit_soa(request, soaid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in soa.changed_data)) messages.success(request, "SOA modifié") return redirect(reverse('machines:index-extension')) - return form({'soaform': soa}, 'machines/machine.html', request) + return form({'soaform': soa, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_soa(request): - soa = DelSOAForm(request.POST or None) +@can_delete_set(SOA) +def del_soa(request, instances): + soa = DelSOAForm(request.POST or None, instances=instances) if soa.is_valid(): soa_dels = soa.cleaned_data['soa'] for soa_del in soa_dels: @@ -610,11 +615,12 @@ def del_soa(request): except ProtectedError: messages.error(request, "Erreur le SOA suivant %s ne peut être supprimé" % soa_del) return redirect(reverse('machines:index-extension')) - return form({'soaform': soa}, 'machines/machine.html', request) + return form({'soaform': soa, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(Mx) def add_mx(request): + mx = MxForm(request.POST or None) if mx.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -623,16 +629,12 @@ def add_mx(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement mx a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'mxform': mx}, 'machines/machine.html', request) + return form({'mxform': mx, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_mx(request, mxid): - try: - mx_instance = Mx.objects.get(pk=mxid) - except Mx.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-extension')) +@can_edit(Mx) +def edit_mx(request, mx_instance, mxid): + mx = MxForm(request.POST or None, instance=mx_instance) if mx.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -641,12 +643,12 @@ def edit_mx(request, mxid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in mx.changed_data)) messages.success(request, "Mx modifié") return redirect(reverse('machines:index-extension')) - return form({'mxform': mx}, 'machines/machine.html', request) + return form({'mxform': mx, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_mx(request): - mx = DelMxForm(request.POST or None) +@can_delete_set(Mx) +def del_mx(request, instances): + mx = DelMxForm(request.POST or None, instances=instances) if mx.is_valid(): mx_dels = mx.cleaned_data['mx'] for mx_del in mx_dels: @@ -658,11 +660,12 @@ def del_mx(request): except ProtectedError: messages.error(request, "Erreur le Mx suivant %s ne peut être supprimé" % mx_del) return redirect(reverse('machines:index-extension')) - return form({'mxform': mx}, 'machines/machine.html', request) + return form({'mxform': mx, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(Ns) def add_ns(request): + ns = NsForm(request.POST or None) if ns.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -671,16 +674,12 @@ def add_ns(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement ns a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'nsform': ns}, 'machines/machine.html', request) + return form({'nsform': ns, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_ns(request, nsid): - try: - ns_instance = Ns.objects.get(pk=nsid) - except Ns.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-extension')) +@can_edit(Ns) +def edit_ns(request, ns_instance, nsid): + ns = NsForm(request.POST or None, instance=ns_instance) if ns.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -689,12 +688,12 @@ def edit_ns(request, nsid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ns.changed_data)) messages.success(request, "Ns modifié") return redirect(reverse('machines:index-extension')) - return form({'nsform': ns}, 'machines/machine.html', request) + return form({'nsform': ns, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_ns(request): - ns = DelNsForm(request.POST or None) +@can_delete_set(Ns) +def del_ns(request, instances): + ns = DelNsForm(request.POST or None, instances=instances) if ns.is_valid(): ns_dels = ns.cleaned_data['ns'] for ns_del in ns_dels: @@ -706,11 +705,12 @@ def del_ns(request): except ProtectedError: messages.error(request, "Erreur le Ns suivant %s ne peut être supprimé" % ns_del) return redirect(reverse('machines:index-extension')) - return form({'nsform': ns}, 'machines/machine.html', request) + return form({'nsform': ns, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(Txt) def add_txt(request): + txt = TxtForm(request.POST or None) if txt.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -719,16 +719,12 @@ def add_txt(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement text a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'txtform': txt}, 'machines/machine.html', request) + return form({'txtform': txt, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_txt(request, txtid): - try: - txt_instance = Txt.objects.get(pk=txtid) - except Txt.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-extension')) +@can_edit(Txt) +def edit_txt(request, txt_instance, txtid): + txt = TxtForm(request.POST or None, instance=txt_instance) if txt.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -737,12 +733,12 @@ def edit_txt(request, txtid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in txt.changed_data)) messages.success(request, "Txt modifié") return redirect(reverse('machines:index-extension')) - return form({'txtform': txt}, 'machines/machine.html', request) + return form({'txtform': txt, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_txt(request): - txt = DelTxtForm(request.POST or None) +@can_delete_set(Txt) +def del_txt(request, instances): + txt = DelTxtForm(request.POST or None, instances=instances) if txt.is_valid(): txt_dels = txt.cleaned_data['txt'] for txt_del in txt_dels: @@ -754,11 +750,12 @@ def del_txt(request): except ProtectedError: messages.error(request, "Erreur le Txt suivant %s ne peut être supprimé" % txt_del) return redirect(reverse('machines:index-extension')) - return form({'txtform': txt}, 'machines/machine.html', request) + return form({'txtform': txt, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(Srv) def add_srv(request): + srv = SrvForm(request.POST or None) if srv.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -767,16 +764,12 @@ def add_srv(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement srv a été ajouté") return redirect(reverse('machines:index-extension')) - return form({'srvform': srv}, 'machines/machine.html', request) + return form({'srvform': srv, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_srv(request, srvid): - try: - srv_instance = Srv.objects.get(pk=srvid) - except Srv.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-extension')) +@can_edit(Srv) +def edit_srv(request, srv_instance, srvid): + srv = SrvForm(request.POST or None, instance=srv_instance) if srv.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -785,12 +778,12 @@ def edit_srv(request, srvid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in srv.changed_data)) messages.success(request, "Srv modifié") return redirect(reverse('machines:index-extension')) - return form({'srvform': srv}, 'machines/machine.html', request) + return form({'srvform': srv, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_srv(request): - srv = DelSrvForm(request.POST or None) +@can_delete_set(Srv) +def del_srv(request, instances): + srv = DelSrvForm(request.POST or None, instances=instances) if srv.is_valid(): srv_dels = srv.cleaned_data['srv'] for srv_del in srv_dels: @@ -802,31 +795,14 @@ def del_srv(request): except ProtectedError: messages.error(request, "Erreur le Srv suivant %s ne peut être supprimé" % srv_del) return redirect(reverse('machines:index-extension')) - return form({'srvform': srv}, 'machines/machine.html', request) + return form({'srvform': srv, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -def add_alias(request, interfaceid): - try: - interface = Interface.objects.get(pk=interfaceid) - except Interface.DoesNotExist: - messages.error(request, u"Interface inexistante" ) - return redirect(reverse('machines:index')) - if not request.user.has_perms(('cableur',)): - options, created = OptionalMachine.objects.get_or_create() - max_lambdauser_aliases = options.max_lambdauser_aliases - if interface.machine.user != request.user: - messages.error(request, "Vous ne pouvez pas ajouter un alias à une machine d'un autre user que vous sans droit") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - if Domain.objects.filter(cname__in=Domain.objects.filter(interface_parent__in=interface.machine.user.user_interfaces())).count() >= max_lambdauser_aliases: - messages.error(request, "Vous avez atteint le maximum d'alias autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_aliases) - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - alias = AliasForm(request.POST or None, infra=request.user.has_perms(('infra',))) +@can_create(Domain) +@can_edit(Interface) +def add_alias(request, interface, interfaceid): + + alias = AliasForm(request.POST or None, user=request.user) if alias.is_valid(): alias = alias.save(commit=False) alias.cname = interface.domain @@ -836,50 +812,31 @@ def add_alias(request, interfaceid): reversion.set_comment("Création") messages.success(request, "Cet alias a été ajouté") return redirect(reverse( - 'machines:index-alias', + 'machines:index-alias', kwargs={'interfaceid':str(interfaceid)} )) - return form({'aliasform': alias}, 'machines/machine.html', request) + return form({'aliasform': alias, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -def edit_alias(request, aliasid): - try: - alias_instance = Domain.objects.get(pk=aliasid) - except Domain.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-extension')) - if not request.user.has_perms(('cableur',)) and alias_instance.cname.interface_parent.machine.user != request.user: - messages.error(request, "Vous ne pouvez pas ajouter un alias à une machine d'un autre user que vous sans droit") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - alias = AliasForm(request.POST or None, instance=alias_instance, infra=request.user.has_perms(('infra',))) +@can_edit(Domain) +def edit_alias(request, domain_instance, domainid): + + alias = AliasForm(request.POST or None, instance=domain_instance, user=request.user) if alias.is_valid(): with transaction.atomic(), reversion.create_revision(): - alias_instance = alias.save() + domain_instance = alias.save() reversion.set_user(request.user) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in alias.changed_data)) messages.success(request, "Alias modifié") return redirect(reverse( - 'machines:index-alias', - kwargs={'interfaceid':str(alias_instance.cname.interface_parent.id)} + 'machines:index-alias', + kwargs={'interfaceid':str(domain_instance.cname.interface_parent.id)} )) - return form({'aliasform': alias}, 'machines/machine.html', request) + return form({'aliasform': alias, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -def del_alias(request, interfaceid): - try: - interface = Interface.objects.get(pk=interfaceid) - except Interface.DoesNotExist: - messages.error(request, u"Interface inexistante" ) - return redirect(reverse('machines:index')) - if not request.user.has_perms(('cableur',)) and interface.machine.user != request.user: - messages.error(request, "Vous ne pouvez pas ajouter un alias à une machine d'un autre user que vous sans droit") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) +@can_edit(Interface) +def del_alias(request, interface, interfaceid): alias = DelAliasForm(request.POST or None, interface=interface) if alias.is_valid(): alias_dels = alias.cleaned_data['alias'] @@ -892,15 +849,16 @@ def del_alias(request, interfaceid): except ProtectedError: messages.error(request, "Erreur l'alias suivant %s ne peut être supprimé" % alias_del) return redirect(reverse( - 'machines:index-alias', + 'machines:index-alias', kwargs={'interfaceid':str(interfaceid)} )) - return form({'aliasform': alias}, 'machines/machine.html', request) + return form({'aliasform': alias, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(Service) def add_service(request): + service = ServiceForm(request.POST or None) if service.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -909,16 +867,12 @@ def add_service(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement service a été ajouté") return redirect(reverse('machines:index-service')) - return form({'serviceform': service}, 'machines/machine.html', request) + return form({'serviceform': service, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_service(request, serviceid): - try: - service_instance = Service.objects.get(pk=serviceid) - except Ns.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-extension')) +@can_edit(Service) +def edit_service(request, service_instance, serviceid): + service = ServiceForm(request.POST or None, instance=service_instance) if service.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -927,12 +881,12 @@ def edit_service(request, serviceid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in service.changed_data)) messages.success(request, "Service modifié") return redirect(reverse('machines:index-service')) - return form({'serviceform': service}, 'machines/machine.html', request) + return form({'serviceform': service, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_service(request): - service = DelServiceForm(request.POST or None) +@can_delete_set(Service) +def del_service(request, instances): + service = DelServiceForm(request.POST or None, instances=instances) if service.is_valid(): service_dels = service.cleaned_data['service'] for service_del in service_dels: @@ -944,11 +898,12 @@ def del_service(request): except ProtectedError: messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % service_del) return redirect(reverse('machines:index-service')) - return form({'serviceform': service}, 'machines/machine.html', request) + return form({'serviceform': service, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(Vlan) def add_vlan(request): + vlan = VlanForm(request.POST or None) if vlan.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -957,16 +912,12 @@ def add_vlan(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement vlan a été ajouté") return redirect(reverse('machines:index-vlan')) - return form({'vlanform': vlan}, 'machines/machine.html', request) + return form({'vlanform': vlan, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_vlan(request, vlanid): - try: - vlan_instance = Vlan.objects.get(pk=vlanid) - except Vlan.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-vlan')) +@can_edit(Vlan) +def edit_vlan(request, vlan_instance, vlanid): + vlan = VlanForm(request.POST or None, instance=vlan_instance) if vlan.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -975,12 +926,12 @@ def edit_vlan(request, vlanid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in vlan.changed_data)) messages.success(request, "Vlan modifié") return redirect(reverse('machines:index-vlan')) - return form({'vlanform': vlan}, 'machines/machine.html', request) + return form({'vlanform': vlan, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_vlan(request): - vlan = DelVlanForm(request.POST or None) +@can_delete_set(Vlan) +def del_vlan(request, instances): + vlan = DelVlanForm(request.POST or None, instances=instances) if vlan.is_valid(): vlan_dels = vlan.cleaned_data['vlan'] for vlan_del in vlan_dels: @@ -992,11 +943,12 @@ def del_vlan(request): except ProtectedError: messages.error(request, "Erreur le Vlan suivant %s ne peut être supprimé" % vlan_del) return redirect(reverse('machines:index-vlan')) - return form({'vlanform': vlan}, 'machines/machine.html', request) + return form({'vlanform': vlan, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') +@can_create(Nas) def add_nas(request): + nas = NasForm(request.POST or None) if nas.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -1005,16 +957,12 @@ def add_nas(request): reversion.set_comment("Création") messages.success(request, "Cet enregistrement nas a été ajouté") return redirect(reverse('machines:index-nas')) - return form({'nasform': nas}, 'machines/machine.html', request) + return form({'nasform': nas, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def edit_nas(request, nasid): - try: - nas_instance = Nas.objects.get(pk=nasid) - except Nas.DoesNotExist: - messages.error(request, u"Entrée inexistante" ) - return redirect(reverse('machines:index-nas')) +@can_edit(Nas) +def edit_nas(request, nas_instance, nasid): + nas = NasForm(request.POST or None, instance=nas_instance) if nas.is_valid(): with transaction.atomic(), reversion.create_revision(): @@ -1023,12 +971,12 @@ def edit_nas(request, nasid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in nas.changed_data)) messages.success(request, "Nas modifié") return redirect(reverse('machines:index-nas')) - return form({'nasform': nas}, 'machines/machine.html', request) + return form({'nasform': nas, 'action_name' : 'Editer'}, 'machines/machine.html', request) @login_required -@permission_required('infra') -def del_nas(request): - nas = DelNasForm(request.POST or None) +@can_delete_set(Nas) +def del_nas(request, instances): + nas = DelNasForm(request.POST or None, instances=instances) if nas.is_valid(): nas_dels = nas.cleaned_data['nas'] for nas_del in nas_dels: @@ -1040,14 +988,13 @@ def del_nas(request): except ProtectedError: messages.error(request, "Erreur le Nas suivant %s ne peut être supprimé" % nas_del) return redirect(reverse('machines:index-nas')) - return form({'nasform': nas}, 'machines/machine.html', request) + return form({'nasform': nas, 'action_name' : 'Supprimer'}, 'machines/machine.html', request) @login_required -@permission_required('cableur') +@can_view_all(Machine) def index(request): - options, created = GeneralOption.objects.get_or_create() - pagination_large_number = options.pagination_large_number - machines_list = Machine.objects.select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type').prefetch_related('interface_set__type__ip_type__extension').prefetch_related('interface_set__domain__related_domain__extension') + pagination_large_number = GeneralOption.get_cached_value('pagination_large_number') + machines_list = Machine.objects.select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type').prefetch_related('interface_set__type__ip_type__extension').prefetch_related('interface_set__domain__related_domain__extension').prefetch_related('interface_set__ipv6list') machines_list = SortTable.sort( machines_list, request.GET.get('col'), @@ -1067,31 +1014,36 @@ def index(request): return render(request, 'machines/index.html', {'machines_list': machines_list}) @login_required -@permission_required('cableur') +@can_view_all(IpType) def index_iptype(request): iptype_list = IpType.objects.select_related('extension').select_related('vlan').order_by('type') return render(request, 'machines/index_iptype.html', {'iptype_list':iptype_list}) @login_required -@permission_required('cableur') +@can_view_all(Vlan) def index_vlan(request): vlan_list = Vlan.objects.prefetch_related('iptype_set').order_by('vlan_id') return render(request, 'machines/index_vlan.html', {'vlan_list':vlan_list}) @login_required -@permission_required('cableur') +@can_view_all(MachineType) def index_machinetype(request): machinetype_list = MachineType.objects.select_related('ip_type').order_by('type') return render(request, 'machines/index_machinetype.html', {'machinetype_list':machinetype_list}) @login_required -@permission_required('cableur') +@can_view_all(Nas) def index_nas(request): nas_list = Nas.objects.select_related('machine_type').select_related('nas_type').order_by('name') return render(request, 'machines/index_nas.html', {'nas_list':nas_list}) @login_required -@permission_required('cableur') +@can_view_all(SOA) +@can_view_all(Mx) +@can_view_all(Ns) +@can_view_all(Txt) +@can_view_all(Srv) +@can_view_all(Extension) def index_extension(request): extension_list = Extension.objects.select_related('origin').select_related('soa').order_by('name') soa_list = SOA.objects.order_by('name') @@ -1102,153 +1054,27 @@ def index_extension(request): return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'soa_list': soa_list, 'mx_list': mx_list, 'ns_list': ns_list, 'txt_list' : txt_list, 'srv_list': srv_list}) @login_required -def index_alias(request, interfaceid): - try: - interface = Interface.objects.get(pk=interfaceid) - except Interface.DoesNotExist: - messages.error(request, u"Interface inexistante" ) - return redirect(reverse('machines:index')) - if not request.user.has_perms(('cableur',)) and interface.machine.user != request.user: - messages.error(request, "Vous ne pouvez pas éditer une machine d'un autre user que vous sans droit") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) +@can_edit(Interface) +def index_alias(request, interface, interfaceid): alias_list = Domain.objects.filter(cname=Domain.objects.filter(interface_parent=interface)).order_by('name') return render(request, 'machines/index_alias.html', {'alias_list':alias_list, 'interface_id': interfaceid}) @login_required -@permission_required('cableur') +@can_edit(Interface) +def index_ipv6(request, interface, interfaceid): + ipv6_list = Ipv6List.objects.filter(interface=interface) + return render(request, 'machines/index_ipv6.html', {'ipv6_list':ipv6_list, 'interface_id': interfaceid}) + +@login_required +@can_view_all(Service) def index_service(request): service_list = Service.objects.prefetch_related('service_link_set__server__domain__extension').all() servers_list = Service_link.objects.select_related('server__domain__extension').select_related('service').all() return render(request, 'machines/index_service.html', {'service_list':service_list, 'servers_list':servers_list}) -@login_required -def history(request, object, id): - if object == 'machine': - try: - object_instance = Machine.objects.get(pk=id) - except Machine.DoesNotExist: - messages.error(request, "Machine inexistante") - return redirect(reverse('machines:index')) - if not request.user.has_perms(('cableur',)) and object_instance.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher l'historique d'une machine d'un autre user que vous sans droit cableur") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - elif object == 'interface': - try: - object_instance = Interface.objects.get(pk=id) - except Interface.DoesNotExist: - messages.error(request, "Interface inexistante") - return redirect(reverse('machines:index')) - if not request.user.has_perms(('cableur',)) and object_instance.machine.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher l'historique d'une interface d'un autre user que vous sans droit cableur") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - elif object == 'alias': - try: - object_instance = Domain.objects.get(pk=id) - except Domain.DoesNotExist: - messages.error(request, "Alias inexistant") - return redirect(reverse('machines:index')) - if not request.user.has_perms(('cableur',)) and object_instance.cname.interface_parent.machine.user != request.user: - messages.error(request, "Vous ne pouvez pas afficher l'historique d'un alias d'un autre user que vous sans droit cableur") - return redirect(reverse( - 'users:profil', - kwargs={'userid':str(request.user.id)} - )) - elif object == 'machinetype' and request.user.has_perms(('cableur',)): - try: - object_instance = MachineType.objects.get(pk=id) - except MachineType.DoesNotExist: - messages.error(request, "Type de machine inexistant") - return redirect(reverse('machines:index')) - elif object == 'iptype' and request.user.has_perms(('cableur',)): - try: - object_instance = IpType.objects.get(pk=id) - except IpType.DoesNotExist: - messages.error(request, "Type d'ip inexistant") - return redirect(reverse('machines:index')) - elif object == 'extension' and request.user.has_perms(('cableur',)): - try: - object_instance = Extension.objects.get(pk=id) - except Extension.DoesNotExist: - messages.error(request, "Extension inexistante") - return redirect(reverse('machines:index')) - elif object == 'soa' and request.user.has_perms(('cableur',)): - try: - object_instance = SOA.objects.get(pk=id) - except SOA.DoesNotExist: - messages.error(request, "SOA inexistant") - return redirect(reverse('machines:index')) - elif object == 'mx' and request.user.has_perms(('cableur',)): - try: - object_instance = Mx.objects.get(pk=id) - except Mx.DoesNotExist: - messages.error(request, "Mx inexistant") - return redirect(reverse('machines:index')) - elif object == 'txt' and request.user.has_perms(('cableur',)): - try: - object_instance = Txt.objects.get(pk=id) - except Txt.DoesNotExist: - messages.error(request, "Txt inexistant") - return redirect(reverse('machines:index')) - elif object == 'srv' and request.user.has_perms(('cableur',)): - try: - object_instance = Srv.objects.get(pk=id) - except Srv.DoesNotExist: - messages.error(request, "Srv inexistant") - return redirect(reverse('machines:index')) - elif object == 'ns' and request.user.has_perms(('cableur',)): - try: - object_instance = Ns.objects.get(pk=id) - except Ns.DoesNotExist: - messages.error(request, "Ns inexistant") - return redirect(reverse('machines:index')) - elif object == 'service' and request.user.has_perms(('cableur',)): - try: - object_instance = Service.objects.get(pk=id) - except Service.DoesNotExist: - messages.error(request, "Service inexistant") - return redirect(reverse('machines:index')) - elif object == 'vlan' and request.user.has_perms(('cableur',)): - try: - object_instance = Vlan.objects.get(pk=id) - except Vlan.DoesNotExist: - messages.error(request, "Vlan inexistant") - return redirect(reverse('machines:index')) - elif object == 'nas' and request.user.has_perms(('cableur',)): - try: - object_instance = Nas.objects.get(pk=id) - except Nas.DoesNotExist: - messages.error(request, "Nas inexistant") - return redirect(reverse('machines:index')) - else: - messages.error(request, "Objet inconnu") - return redirect(reverse('machines:index')) - options, created = GeneralOption.objects.get_or_create() - pagination_number = options.pagination_number - reversions = Version.objects.get_for_object(object_instance) - paginator = Paginator(reversions, pagination_number) - page = request.GET.get('page') - try: - reversions = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - reversions = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance}) - @login_required -@permission_required('cableur') +@can_view_all(OuverturePortList) def index_portlist(request): port_list = OuverturePortList.objects.prefetch_related('ouvertureport_set')\ .prefetch_related('interface_set__domain__extension')\ @@ -1256,14 +1082,10 @@ def index_portlist(request): return render(request, "machines/index_portlist.html", {'port_list':port_list}) @login_required -@permission_required('bureau') -def edit_portlist(request, pk): - try: - port_list_instance = OuverturePortList.objects.get(pk=pk) - except OuverturePortList.DoesNotExist: - messages.error(request, "Liste de ports inexistante") - return redirect(reverse('machines:index-portlist')) - port_list = EditOuverturePortListForm(request.POST or None, instance=port_list_instance) +@can_edit(OuverturePortList) +def edit_portlist(request, ouvertureportlist_instance, ouvertureportlistid): + + port_list = EditOuverturePortListForm(request.POST or None, instance=ouvertureportlist_instance) port_formset = modelformset_factory( OuverturePort, fields=('begin','end','protocole','io'), @@ -1271,7 +1093,7 @@ def edit_portlist(request, pk): can_delete=True, min_num=1, validate_min=True, - )(request.POST or None, queryset=port_list_instance.ouvertureport_set.all()) + )(request.POST or None, queryset=ouvertureportlist_instance.ouvertureport_set.all()) if port_list.is_valid() and port_formset.is_valid(): pl = port_list.save() instances = port_formset.save(commit=False) @@ -1285,23 +1107,16 @@ def edit_portlist(request, pk): return form({'port_list' : port_list, 'ports' : port_formset}, 'machines/edit_portlist.html', request) @login_required -@permission_required('bureau') -def del_portlist(request, pk): - try: - port_list_instance = OuverturePortList.objects.get(pk=pk) - except OuverturePortList.DoesNotExist: - messages.error(request, "Liste de ports inexistante") - return redirect(reverse('machines:index-portlist')) - if port_list_instance.interface_set.all(): - messages.error(request, "Cette liste de ports est utilisée") - return redirect(reverse('machines:index-portlist')) +@can_delete(OuverturePortList) +def del_portlist(request, port_list_instance, ouvertureportlistid): port_list_instance.delete() messages.success(request, "La liste de ports a été supprimée") return redirect(reverse('machines:index-portlist')) @login_required -@permission_required('bureau') +@can_create(OuverturePortList) def add_portlist(request): + port_list = EditOuverturePortListForm(request.POST or None) port_formset = modelformset_factory( OuverturePort, @@ -1327,16 +1142,12 @@ def add_portlist(request): port_list.save() messages.success(request, "Liste de ports créée") return redirect(reverse('machines:index-portlist')) - return form({'machineform' : port_list}, 'machines/machine.html', request) + return form({'machineform' : port_list, 'action_name' : 'Créer'}, 'machines/machine.html', request) @login_required -@permission_required('cableur') -def configure_ports(request, pk): - try: - interface_instance = Interface.objects.get(pk=pk) - except Interface.DoesNotExist: - messages.error(request, u"Interface inexistante" ) - return redirect(reverse('machines:index')) +@can_create(OuverturePort) +@can_edit(Interface) +def configure_ports(request, interface_instance, interfaceid): if not interface_instance.may_have_port_open(): messages.error(request, "Attention, l'ipv4 n'est pas publique, l'ouverture n'aura pas d'effet en v4") interface = EditOuverturePortConfigForm(request.POST or None, instance=interface_instance) @@ -1344,7 +1155,7 @@ def configure_ports(request, pk): interface.save() messages.success(request, "Configuration des ports mise à jour.") return redirect(reverse('machines:index')) - return form({'interfaceform' : interface}, 'machines/machine.html', request) + return form({'interfaceform' : interface, 'action_name' : 'Editer la configuration'}, 'machines/machine.html', request) """ Framework Rest """ @@ -1356,7 +1167,7 @@ class JSONResponse(HttpResponse): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def mac_ip_list(request): interfaces = all_active_assigned_interfaces() seria = InterfaceSerializer(interfaces, many=True) @@ -1364,15 +1175,15 @@ def mac_ip_list(request): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def full_mac_ip_list(request): - interfaces = all_active_assigned_interfaces() + interfaces = all_active_assigned_interfaces(full=True) seria = FullInterfaceSerializer(interfaces, many=True) return seria.data @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def alias(request): alias = Domain.objects.filter(interface_parent=None).filter(cname__in=Domain.objects.filter(interface_parent__in=Interface.objects.exclude(ipv4=None))).select_related('extension').select_related('cname__extension') seria = DomainSerializer(alias, many=True) @@ -1380,7 +1191,7 @@ def alias(request): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def corresp(request): type = IpType.objects.all().select_related('extension') seria = TypeSerializer(type, many=True) @@ -1388,7 +1199,7 @@ def corresp(request): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def mx(request): mx = Mx.objects.all().select_related('zone').select_related('name__extension') seria = MxSerializer(mx, many=True) @@ -1396,7 +1207,7 @@ def mx(request): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def txt(request): txt = Txt.objects.all().select_related('zone') seria = TxtSerializer(txt, many=True) @@ -1404,7 +1215,7 @@ def txt(request): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def srv(request): srv = Srv.objects.all().select_related('extension').select_related('target__extension') seria = SrvSerializer(srv, many=True) @@ -1412,7 +1223,7 @@ def srv(request): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def ns(request): ns = Ns.objects.exclude(ns__in=Domain.objects.filter(interface_parent__in=Interface.objects.filter(ipv4=None))).select_related('zone').select_related('ns__extension') seria = NsSerializer(ns, many=True) @@ -1420,7 +1231,7 @@ def ns(request): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def zones(request): zones = Extension.objects.all().select_related('origin') seria = ExtensionSerializer(zones, many=True) @@ -1428,21 +1239,21 @@ def zones(request): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def mac_ip(request): seria = mac_ip_list(request) return JSONResponse(seria) @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def mac_ip_dns(request): seria = full_mac_ip_list(request) return JSONResponse(seria) @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def service_servers(request): service_link = Service_link.objects.all().select_related('server__domain').select_related('service') seria = ServiceServersSerializer(service_link, many=True) @@ -1450,7 +1261,7 @@ def service_servers(request): @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def ouverture_ports(request): r = {'ipv4':{}, 'ipv6':{}} for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set', 'interface_set__ipv4'): @@ -1468,17 +1279,19 @@ def ouverture_ports(request): d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) r['ipv4'][i.ipv4.ipv4] = d - if i.ipv6_object: - d = r['ipv6'].get(i.ipv6, {}) - d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) - d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) - d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) - d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) - r['ipv6'][i.ipv6] = d + if i.ipv6(): + for ipv6 in i.ipv6(): + d = r['ipv6'].get(ipv6.ipv6, {}) + d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"]) + d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"]) + d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"]) + d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"]) + r['ipv6'][ipv6.ipv6] = d return JSONResponse(r) + @csrf_exempt @login_required -@permission_required('serveur') +@permission_required('machines.serveur') def regen_achieved(request): obj = Service_link.objects.filter(service__in=Service.objects.filter(service_type=request.POST['service']), server__in=Interface.objects.filter(domain__in=Domain.objects.filter(name=request.POST['server']))) if obj: diff --git a/preferences/__init__.py b/preferences/__init__.py index e69de29b..e895e295 100644 --- a/preferences/__init__.py +++ b/preferences/__init__.py @@ -0,0 +1,2 @@ + +from .acl import * diff --git a/preferences/acl.py b/preferences/acl.py new file mode 100644 index 00000000..8ffb4c9b --- /dev/null +++ b/preferences/acl.py @@ -0,0 +1,40 @@ +# -*- mode: python; coding: utf-8 -*- +# 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. + +"""preferences.acl + +Here are defined some functions to check acl on the application. +""" + +def can_view(user): + """Check if an user can view the application. + + Args: + user: The user who wants to view the application. + + Returns: + A couple (allowed, msg) where allowed is a boolean which is True if + viewing is granted and msg is a message (can be None). + """ + can = user.has_module_perms('preferences') + return can, None if can else "Vous ne pouvez pas voir cette application." diff --git a/preferences/aes_field.py b/preferences/aes_field.py new file mode 100644 index 00000000..ce90e1f4 --- /dev/null +++ b/preferences/aes_field.py @@ -0,0 +1,56 @@ +import string +import binascii +from random import choice +from Crypto.Cipher import AES + +from django.db import models +from django.conf import settings + +EOD = '`%EofD%`' # This should be something that will not occur in strings + + +def genstring(length=16, chars=string.printable): + return ''.join([choice(chars) for i in range(length)]) + + +def encrypt(key, s): + obj = AES.new(key) + datalength = len(s) + len(EOD) + if datalength < 16: + saltlength = 16 - datalength + else: + saltlength = 16 - datalength % 16 + ss = ''.join([s, EOD, genstring(saltlength)]) + return obj.encrypt(ss) + + +def decrypt(key, s): + obj = AES.new(key) + ss = obj.decrypt(s) + return ss.split(bytes(EOD, 'utf-8'))[0] + + +class AESEncryptedField(models.CharField): + def save_form_data(self, instance, data): + setattr(instance, self.name, + binascii.b2a_base64(encrypt(settings.AES_KEY, data))) + + def to_python(self, value): + if value is None: + return None + return decrypt(settings.AES_KEY, + binascii.a2b_base64(value)).decode('utf-8') + + def from_db_value(self, value, expression, connection, *args): + if value is None: + return value + return decrypt(settings.AES_KEY, + binascii.a2b_base64(value)).decode('utf-8') + + def get_prep_value(self, value): + if value is None: + return value + return binascii.b2a_base64(encrypt( + settings.AES_KEY, + value + )) diff --git a/preferences/forms.py b/preferences/forms.py index 51cbb885..fc214f52 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -48,6 +48,9 @@ class EditOptionalUserForm(ModelForm): téléphone' self.fields['user_solde'].label = 'Activation du solde pour\ les utilisateurs' + self.fields['max_solde'].label = 'Solde maximum' + self.fields['min_online_payment'].label = 'Montant de rechargement minimum en ligne' + self.fields['self_adhesion'].label = 'Auto inscription' class EditOptionalMachineForm(ModelForm): @@ -114,6 +117,7 @@ class EditGeneralOptionForm(ModelForm): self.fields['site_name'].label = 'Nom du site web' self.fields['email_from'].label = "Adresse mail d\ 'expedition automatique" + self.fields['GTU_sum_up'].label = "Résumé des CGU" class EditAssoOptionForm(ModelForm): @@ -173,7 +177,15 @@ class ServiceForm(ModelForm): class DelServiceForm(Form): """Suppression de services sur la page d'accueil""" services = forms.ModelMultipleChoiceField( - queryset=Service.objects.all(), + queryset=Service.objects.none(), label="Enregistrements service actuels", widget=forms.CheckboxSelectMultiple ) + + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelServiceForm, self).__init__(*args, **kwargs) + if instances: + self.fields['services'].queryset = instances + else: + self.fields['services'].queryset = Service.objects.all() diff --git a/preferences/migrations/0025_auto_20171231_2142.py b/preferences/migrations/0025_auto_20171231_2142.py new file mode 100644 index 00000000..d54b8215 --- /dev/null +++ b/preferences/migrations/0025_auto_20171231_2142.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-31 20:42 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0024_optionaluser_all_can_create'), + ] + + operations = [ + migrations.AlterModelOptions( + name='assooption', + options={'permissions': (('view_assooption', "Peut voir les options de l'asso"),)}, + ), + migrations.AlterModelOptions( + name='generaloption', + options={'permissions': (('view_generaloption', 'Peut voir les options générales'),)}, + ), + migrations.AlterModelOptions( + name='mailmessageoption', + options={'permissions': (('view_mailmessageoption', 'Peut voir les options de mail'),)}, + ), + migrations.AlterModelOptions( + name='optionalmachine', + options={'permissions': (('view_optionalmachine', 'Peut voir les options de machine'),)}, + ), + migrations.AlterModelOptions( + name='optionaltopologie', + options={'permissions': (('view_optionaltopologie', 'Peut voir les options de topologie'),)}, + ), + migrations.AlterModelOptions( + name='optionaluser', + options={'permissions': (('view_optionaluser', "Peut voir les options de l'user"),)}, + ), + migrations.AlterModelOptions( + name='service', + options={'permissions': (('view_service', 'Peut voir les options de service'),)}, + ), + ] diff --git a/preferences/migrations/0027_merge_20180106_2019.py b/preferences/migrations/0027_merge_20180106_2019.py new file mode 100644 index 00000000..68ce34fb --- /dev/null +++ b/preferences/migrations/0027_merge_20180106_2019.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-06 19:19 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0025_auto_20171231_2142'), + ('preferences', '0026_auto_20171216_0401'), + ] + + operations = [ + ] diff --git a/preferences/migrations/0028_assooption_description.py b/preferences/migrations/0028_assooption_description.py new file mode 100644 index 00000000..7c955d86 --- /dev/null +++ b/preferences/migrations/0028_assooption_description.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-08 14:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0027_merge_20180106_2019'), + ('preferences', '0043_optionalmachine_create_machine'), + ] + + operations = [ + migrations.AddField( + model_name='assooption', + name='description', + field=models.TextField(default=''), + ), + ] diff --git a/preferences/migrations/0028_auto_20180111_1129.py b/preferences/migrations/0028_auto_20180111_1129.py new file mode 100644 index 00000000..c6c03719 --- /dev/null +++ b/preferences/migrations/0028_auto_20180111_1129.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-11 10:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0027_merge_20180106_2019'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='max_recharge', + field=models.DecimalField(decimal_places=2, default=100, max_digits=5), + ), + ] diff --git a/preferences/migrations/0028_auto_20180128_2203.py b/preferences/migrations/0028_auto_20180128_2203.py new file mode 100644 index 00000000..ac8894fd --- /dev/null +++ b/preferences/migrations/0028_auto_20180128_2203.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-28 21:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0027_merge_20180106_2019'), + ] + + operations = [ + migrations.RemoveField( + model_name='optionalmachine', + name='ipv6', + ), + migrations.AddField( + model_name='optionalmachine', + name='ipv6_mode', + field=models.CharField(choices=[('SLAAC', 'Autoconfiguration par RA'), ('DHCPV6', 'Attribution des ip par dhcpv6'), ('DISABLED', 'Désactivé')], default='DISABLED', max_length=32), + ), + ] diff --git a/preferences/migrations/0029_auto_20180111_1134.py b/preferences/migrations/0029_auto_20180111_1134.py new file mode 100644 index 00000000..92220312 --- /dev/null +++ b/preferences/migrations/0029_auto_20180111_1134.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-11 10:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0028_auto_20180111_1129'), + ] + + operations = [ + migrations.AddField( + model_name='assooption', + name='payment', + field=models.CharField(choices=[('NONE', 'NONE'), ('COMNPAY', 'COMNPAY')], default='NONE', max_length=255), + ), + ] diff --git a/preferences/migrations/0030_auto_20180111_2346.py b/preferences/migrations/0030_auto_20180111_2346.py new file mode 100644 index 00000000..7f912bbd --- /dev/null +++ b/preferences/migrations/0030_auto_20180111_2346.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-11 22:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0029_auto_20180111_1134'), + ] + + operations = [ + migrations.RemoveField( + model_name='optionaluser', + name='max_recharge', + ), + migrations.AddField( + model_name='optionaluser', + name='max_solde', + field=models.DecimalField(decimal_places=2, default=50, max_digits=5), + ), + ] diff --git a/preferences/migrations/0031_optionaluser_self_adhesion.py b/preferences/migrations/0031_optionaluser_self_adhesion.py new file mode 100644 index 00000000..48a95044 --- /dev/null +++ b/preferences/migrations/0031_optionaluser_self_adhesion.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-12 11:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0030_auto_20180111_2346'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='self_adhesion', + field=models.BooleanField(default=False, help_text='Un nouvel utilisateur peut se créer son compte sur re2o'), + ), + ] diff --git a/preferences/migrations/0032_optionaluser_min_online_payment.py b/preferences/migrations/0032_optionaluser_min_online_payment.py new file mode 100644 index 00000000..ef78d012 --- /dev/null +++ b/preferences/migrations/0032_optionaluser_min_online_payment.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-13 16:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0031_optionaluser_self_adhesion'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='min_online_payment', + field=models.DecimalField(decimal_places=2, default=10, max_digits=5), + ), + ] diff --git a/preferences/migrations/0033_generaloption_gtu_sum_up.py b/preferences/migrations/0033_generaloption_gtu_sum_up.py new file mode 100644 index 00000000..63c2df5e --- /dev/null +++ b/preferences/migrations/0033_generaloption_gtu_sum_up.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 19:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0032_optionaluser_min_online_payment'), + ] + + operations = [ + migrations.AddField( + model_name='generaloption', + name='GTU_sum_up', + field=models.TextField(blank=True, default='', help_text='Résumé des CGU'), + ), + ] diff --git a/preferences/migrations/0034_auto_20180114_2025.py b/preferences/migrations/0034_auto_20180114_2025.py new file mode 100644 index 00000000..b6969021 --- /dev/null +++ b/preferences/migrations/0034_auto_20180114_2025.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 19:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0033_generaloption_gtu_sum_up'), + ] + + operations = [ + migrations.AddField( + model_name='generaloption', + name='GTU', + field=models.FileField(default='', upload_to='GTU'), + ), + migrations.AlterField( + model_name='generaloption', + name='GTU_sum_up', + field=models.TextField(blank=True, default=''), + ), + ] diff --git a/preferences/migrations/0035_auto_20180114_2132.py b/preferences/migrations/0035_auto_20180114_2132.py new file mode 100644 index 00000000..e3767828 --- /dev/null +++ b/preferences/migrations/0035_auto_20180114_2132.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 20:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0034_auto_20180114_2025'), + ] + + operations = [ + migrations.AlterField( + model_name='generaloption', + name='GTU', + field=models.FileField(default='', upload_to='/var/www/static/'), + ), + ] diff --git a/preferences/migrations/0036_auto_20180114_2141.py b/preferences/migrations/0036_auto_20180114_2141.py new file mode 100644 index 00000000..1b844ac8 --- /dev/null +++ b/preferences/migrations/0036_auto_20180114_2141.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 20:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0035_auto_20180114_2132'), + ] + + operations = [ + migrations.AlterField( + model_name='generaloption', + name='GTU', + field=models.FileField(default='', upload_to=''), + ), + ] diff --git a/preferences/migrations/0037_auto_20180114_2156.py b/preferences/migrations/0037_auto_20180114_2156.py new file mode 100644 index 00000000..efafa806 --- /dev/null +++ b/preferences/migrations/0037_auto_20180114_2156.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 20:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0036_auto_20180114_2141'), + ] + + operations = [ + migrations.AlterField( + model_name='generaloption', + name='GTU', + field=models.FileField(default='', null=True, upload_to=''), + ), + ] diff --git a/preferences/migrations/0038_auto_20180114_2209.py b/preferences/migrations/0038_auto_20180114_2209.py new file mode 100644 index 00000000..3077ebff --- /dev/null +++ b/preferences/migrations/0038_auto_20180114_2209.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 21:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0037_auto_20180114_2156'), + ] + + operations = [ + migrations.AlterField( + model_name='generaloption', + name='GTU', + field=models.FileField(blank=True, default='', null=True, upload_to=''), + ), + ] diff --git a/preferences/migrations/0039_auto_20180115_0003.py b/preferences/migrations/0039_auto_20180115_0003.py new file mode 100644 index 00000000..3dbe2b4c --- /dev/null +++ b/preferences/migrations/0039_auto_20180115_0003.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-14 23:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import preferences.aes_field + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0038_auto_20180114_2209'), + ] + + operations = [ + migrations.AddField( + model_name='assooption', + name='payment_id', + field=models.CharField(max_length=255, null=True), + ), + ] diff --git a/preferences/migrations/0040_auto_20180129_1745.py b/preferences/migrations/0040_auto_20180129_1745.py new file mode 100644 index 00000000..dc7800f4 --- /dev/null +++ b/preferences/migrations/0040_auto_20180129_1745.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-29 16:45 +from __future__ import unicode_literals + +from django.db import migrations, models +import preferences.aes_field + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0039_auto_20180115_0003'), + ] + + operations = [ + migrations.AddField( + model_name='assooption', + name='payment_pass', + field=preferences.aes_field.AESEncryptedField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='assooption', + name='payment_id', + field=models.CharField(default='', max_length=255), + ), + ] diff --git a/preferences/migrations/0041_merge_20180130_0052.py b/preferences/migrations/0041_merge_20180130_0052.py new file mode 100644 index 00000000..ce2a1e5c --- /dev/null +++ b/preferences/migrations/0041_merge_20180130_0052.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-01-29 23:52 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0028_auto_20180128_2203'), + ('preferences', '0040_auto_20180129_1745'), + ] + + operations = [ + ] diff --git a/preferences/migrations/0042_auto_20180222_1743.py b/preferences/migrations/0042_auto_20180222_1743.py new file mode 100644 index 00000000..b7ab7abe --- /dev/null +++ b/preferences/migrations/0042_auto_20180222_1743.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-02-22 16:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0041_merge_20180130_0052'), + ] + + operations = [ + migrations.RemoveField( + model_name='optionaluser', + name='all_can_create', + ), + migrations.AddField( + model_name='optionaluser', + name='all_can_create_adherent', + field=models.BooleanField(default=False, help_text="Les users peuvent créer d'autres adhérents"), + ), + migrations.AddField( + model_name='optionaluser', + name='all_can_create_club', + field=models.BooleanField(default=False, help_text='Les users peuvent créer un club'), + ), + ] diff --git a/preferences/migrations/0043_optionalmachine_create_machine.py b/preferences/migrations/0043_optionalmachine_create_machine.py new file mode 100644 index 00000000..e01a5878 --- /dev/null +++ b/preferences/migrations/0043_optionalmachine_create_machine.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-02-28 13:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0042_auto_20180222_1743'), + ] + + operations = [ + migrations.AddField( + model_name='optionalmachine', + name='create_machine', + field=models.BooleanField(default=True, help_text="Permet à l'user de créer une machine"), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 2e803b66..efd32f88 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -25,11 +25,36 @@ Reglages généraux, machines, utilisateurs, mail, general pour l'application. """ from __future__ import unicode_literals +from django.utils.functional import cached_property from django.db import models -from cotisations.models import Paiement +import cotisations.models +import machines.models +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver +from django.core.cache import cache + +from .aes_field import AESEncryptedField -class OptionalUser(models.Model): +class PreferencesModel(models.Model): + @classmethod + def set_in_cache(cls): + instance, _created = cls.objects.get_or_create() + cache.set(cls().__class__.__name__.lower(), instance, None) + return instance + + @classmethod + def get_cached_value(cls, key): + instance = cache.get(cls().__class__.__name__.lower()) + if instance == None: + instance = cls.set_in_cache() + return getattr(instance, key) + + class Meta: + abstract = True + + +class OptionalUser(PreferencesModel): """Options pour l'user : obligation ou nom du telephone, activation ou non du solde, autorisation du negatif, fingerprint etc""" PRETTY_NAME = "Options utilisateur" @@ -41,30 +66,204 @@ class OptionalUser(models.Model): decimal_places=2, default=0 ) - gpg_fingerprint = models.BooleanField(default=True) - all_can_create = models.BooleanField( - default=False, - help_text="Tous les users peuvent en créer d'autres", + max_solde = models.DecimalField( + max_digits=5, + decimal_places=2, + default=50 ) + min_online_payment = models.DecimalField( + max_digits=5, + decimal_places=2, + default=10 + ) + gpg_fingerprint = models.BooleanField(default=True) + all_can_create_club = models.BooleanField( + default=False, + help_text="Les users peuvent créer un club" + ) + all_can_create_adherent = models.BooleanField( + default=False, + help_text="Les users peuvent créer d'autres adhérents", + ) + self_adhesion = models.BooleanField( + default=False, + help_text="Un nouvel utilisateur peut se créer son compte sur re2o" + ) + + class Meta: + permissions = ( + ("view_optionaluser", "Peut voir les options de l'user"), + ) + + def get_instance(*args, **kwargs): + return OptionalUser.objects.get_or_create() + + def can_create(user_request, *args, **kwargs): + """Check if an user can create a OptionalUser object. + + :param user_request: The user who wants to create a user object. + :return: a message and a boolean which is True if the user can create. + """ + return user_request.has_perm('preferences.add_optionaluser'), u"Vous n'avez pas le droit\ + de créer les préférences concernant les users" + + def can_edit(self, user_request, *args, **kwargs): + """Check if an user can edit a OptionalUser object. + + :param self: The OptionalUser which is to be edited. + :param user_request: The user who requests to edit self. + :return: a message and a boolean which is True if edition is granted. + """ + return user_request.has_perm('preferences.change_optionaluser'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant les users" + + def can_delete(self, user_request, *args, **kwargs): + """Check if an user can delete a OptionalUser object. + + :param self: The OptionalUser which is to be deleted. + :param user_request: The user who requests deletion. + :return: True if deletion is granted, and a message. + """ + return user_request.has_perm('preferences.delete_optionaluser'), u"Vous n'avez pas le droit\ + de supprimer les préférences concernant les users" + + def can_view_all(user_request, *args, **kwargs): + """Check if an user can access to the list of every OptionalUser objects + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation message. + """ + return user_request.has_perm('preferences.view_optionaluser'), u"Vous n'avez pas le droit\ + de voir les préférences concernant les utilisateurs" + + def can_view(self, user_request, *args, **kwargs): + """Check if an user can view a OptionalUser object. + + :param self: The targeted OptionalUser. + :param user_request: The user who ask for viewing the target. + :return: A boolean telling if the acces is granted and an explanation + text + """ + return user_request.has_perm('preferences.view_optionaluser'), u"Vous n'avez pas le droit\ + de voir les préférences concernant les utilisateurs" def clean(self): """Creation du mode de paiement par solde""" if self.user_solde: - Paiement.objects.get_or_create(moyen="Solde") + p = cotisations.models.Paiement.objects.filter(moyen="Solde") + if not len(p): + c = cotisations.models.Paiement(moyen="Solde") + c.save() -class OptionalMachine(models.Model): +@receiver(post_save, sender=OptionalUser) +def optionaluser_post_save(sender, **kwargs): + """Ecriture dans le cache""" + user_pref = kwargs['instance'] + user_pref.set_in_cache() + + +class OptionalMachine(PreferencesModel): """Options pour les machines : maximum de machines ou d'alias par user sans droit, activation de l'ipv6""" PRETTY_NAME = "Options machines" + SLAAC = 'SLAAC' + DHCPV6 = 'DHCPV6' + DISABLED = 'DISABLED' + CHOICE_IPV6 = ( + (SLAAC, 'Autoconfiguration par RA'), + (DHCPV6, 'Attribution des ip par dhcpv6'), + (DISABLED, 'Désactivé'), + ) + password_machine = models.BooleanField(default=False) max_lambdauser_interfaces = models.IntegerField(default=10) max_lambdauser_aliases = models.IntegerField(default=10) - ipv6 = models.BooleanField(default=False) + ipv6_mode = models.CharField( + max_length=32, + choices=CHOICE_IPV6, + default='DISABLED' + ) + create_machine = models.BooleanField( + default=True, + help_text="Permet à l'user de créer une machine" + ) + + @cached_property + def ipv6(self): + return not self.get_cached_value('ipv6_mode') == 'DISABLED' + + class Meta: + permissions = ( + ("view_optionalmachine", "Peut voir les options de machine"), + ) + + def get_instance(*args, **kwargs): + return OptionalMachine.objects.get_or_create() + + def can_create(user_request, *args, **kwargs): + """Check if an user can create a OptionalMachine object. + + :param user_request: The user who wants to create an object. + :return: a message and a boolean which is True if the user can create. + """ + return user_request.has_perm('preferences.add_optionalmachine'), u"Vous n'avez pas le droit\ + de créer les préférences concernant les machines" + + def can_edit(self, user_request, *args, **kwargs): + """Check if an user can edit a OptionalMachine object. + + :param self: The OptionalMachine which is to be edited. + :param user_request: The user who requests to edit self. + :return: a message and a boolean which is True if edition is granted. + """ + return user_request.has_perm('preferences.change_optionalmachine'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant les machines" + + def can_delete(self, user_request, *args, **kwargs): + """Check if an user can delete a OptionalMachine object. + + :param self: The OptionalMachine which is to be deleted. + :param user_request: The user who requests deletion. + :return: True if deletion is granted, and a message. + """ + + return user_request.has_perm('preferences.delete_optionalmachine'), u"Vous n'avez pas le droit\ + de supprimer les préférences concernant les machines" + + def can_view_all(user_request, *args, **kwargs): + """Check if an user can access to the list of every OptionalMachine objects + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation message. + """ + return user_request.has_perm('preferences.view_optionalmachine'), u"Vous n'avez pas le droit\ + de voir les préférences concernant les machines" + + def can_view(self, user_request, *args, **kwargs): + """Check if an user can view a OptionalMachine object. + + :param self: The targeted OptionalMachine. + :param user_request: The user who ask for viewing the target. + :return: A boolean telling if the acces is granted and an explanation + text + """ + return user_request.has_perm('preferences.view_optionalmachine'), u"Vous n'avez pas le droit\ + de voir les préférences concernant les machines" -class OptionalTopologie(models.Model): +@receiver(post_save, sender=OptionalMachine) +def optionalmachine_post_save(sender, **kwargs): + """Synchronisation ipv6 et ecriture dans le cache""" + machine_pref = kwargs['instance'] + machine_pref.set_in_cache() + if machine_pref.ipv6_mode != "DISABLED": + for interface in machines.models.Interface.objects.all(): + interface.sync_ipv6() + + +class OptionalTopologie(PreferencesModel): """Reglages pour la topologie : mode d'accès radius, vlan où placer les machines en accept ou reject""" PRETTY_NAME = "Options topologie" @@ -96,8 +295,72 @@ class OptionalTopologie(models.Model): null=True ) + class Meta: + permissions = ( + ("view_optionaltopologie", "Peut voir les options de topologie"), + ) -class GeneralOption(models.Model): + def get_instance(*args, **kwargs): + return OptionalTopologie.objects.get_or_create() + + def can_create(user_request, *args, **kwargs): + """Check if an user can create a OptionalTopologie object. + + :param user_request: The user who wants to create an object. + :return: a message and a boolean which is True if the user can create. + """ + return user_request.has_perm('preferences.add_optionaltopologie'), u"Vous n'avez pas le droit\ + de créer les préférences concernant la topologie" + + def can_edit(self, user_request, *args, **kwargs): + """Check if an user can edit a OptionalTopologie object. + + :param self: The OptionalTopologie which is to be edited. + :param user_request: The user who requests to edit self. + :return: a message and a boolean which is True if edition is granted. + """ + return user_request.has_perm('preferences.change_optionaltopologie'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant la topologie" + + def can_delete(self, user_request, *args, **kwargs): + """Check if an user can delete a OptionalTopologie object. + + :param self: The OptionalTopologie which is to be deleted. + :param user_request: The user who requests deletion. + :return: True if deletion is granted, and a message. + """ + return user_request.has_perm('preferences.delete_optionaltoplogie'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant la topologie" + + def can_view_all(user_request, *args, **kwargs): + """Check if an user can access to the list of every OptionalTopologie objects + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation message. + """ + return user_request.has_perm('preferences.view_optionaltopologie'), u"Vous n'avez pas le droit\ + de voir les préférences concernant la topologie" + + def can_view(self, user_request, *args, **kwargs): + """Check if an user can view a OptionalTopologie object. + + :param self: The targeted OptionalTopologie. + :param user_request: The user who ask for viewing the target. + :return: A boolean telling if the acces is granted and an explanation + text + """ + return user_request.has_perm('preferences.view_optionaltopologie'), u"Vous n'avez pas le droit\ + de voir les préférences concernant la topologie" + + +@receiver(post_save, sender=OptionalTopologie) +def optionaltopologie_post_save(sender, **kwargs): + """Ecriture dans le cache""" + topologie_pref = kwargs['instance'] + topologie_pref.set_in_cache() + + +class GeneralOption(PreferencesModel): """Options générales : nombre de resultats par page, nom du site, temps où les liens sont valides""" PRETTY_NAME = "Options générales" @@ -113,6 +376,81 @@ class GeneralOption(models.Model): req_expire_hrs = models.IntegerField(default=48) site_name = models.CharField(max_length=32, default="Re2o") email_from = models.EmailField(default="www-data@serveur.net") + GTU_sum_up = models.TextField( + default="", + blank=True, + ) + GTU = models.FileField( + upload_to = '', + default="", + null=True, + blank=True, + ) + + class Meta: + permissions = ( + ("view_generaloption", "Peut voir les options générales"), + ) + + def get_instance(*args, **kwargs): + return GeneralOption.objects.get_or_create() + + def can_create(user_request, *args, **kwargs): + """Check if an user can create a GeneralOption object. + + :param user_request: The user who wants to create an object. + :return: a message and a boolean which is True if the user can create. + """ + return user_request.has_perm('preferences.add_generaloption'), u"Vous n'avez pas le droit\ + de créer les préférences générales" + + def can_edit(self, user_request, *args, **kwargs): + """Check if an user can edit a GeneralOption object. + + :param self: The GeneralOption which is to be edited. + :param user_request: The user who requests to edit self. + :return: a message and a boolean which is True if edition is granted. + """ + return user_request.has_perm('preferences.change_generaloption'), u"Vous n'avez pas le droit\ + d'éditer les préférences générales" + + def can_delete(self, user_request, *args, **kwargs): + """Check if an user can delete a GeneralOption object. + + :param self: The GeneralOption which is to be deleted. + :param user_request: The user who requests deletion. + :return: True if deletion is granted, and a message. + """ + return user_request.has_perm('preferences.delete_generaloption'), u"Vous n'avez pas le droit\ + d'éditer les préférences générales" + + def can_view_all(user_request, *args, **kwargs): + """Check if an user can access to the list of every GeneralOption objects + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation message. + """ + + return user_request.has_perm('preferences.view_generaloption'), u"Vous n'avez pas le droit\ + de voir les préférences générales" + + def can_view(self, user_request, *args, **kwargs): + """Check if an user can view a GeneralOption object. + + :param self: The targeted GeneralOption. + :param user_request: The user who ask for viewing the target. + :return: A boolean telling if the acces is granted and an explanation + text + """ + return user_request.has_perm('preferences.view_generaloption'), u"Vous n'avez pas le droit\ + de voir les préférences générales" + + +@receiver(post_save, sender=GeneralOption) +def generaloption_post_save(sender, **kwargs): + """Ecriture dans le cache""" + general_pref = kwargs['instance'] + general_pref.set_in_cache() class Service(models.Model): @@ -123,11 +461,70 @@ class Service(models.Model): description = models.TextField() image = models.ImageField(upload_to='logo', blank=True) + class Meta: + permissions = ( + ("view_service", "Peut voir les options de service"), + ) + + def get_instance(serviceid, *args, **kwargs): + return Service.objects.get(pk=serviceid) + + def can_create(user_request, *args, **kwargs): + """Check if an user can create a Service object. + + :param user_request: The user who wants to create an object. + :return: a message and a boolean which is True if the user can create. + """ + + return user_request.has_perm('preferences.add_service'), u"Vous n'avez pas le droit\ + de créer un service pour la page d'accueil" + + def can_edit(self, user_request, *args, **kwargs): + """Check if an user can edit a Service object. + + :param self: The Service which is to be edited. + :param user_request: The user who requests to edit self. + :return: a message and a boolean which is True if edition is granted. + """ + return user_request.has_perm('preferences.change_service'), u"Vous n'avez pas le droit\ + d'éditer les services pour la page d'accueil" + + def can_delete(self, user_request, *args, **kwargs): + """Check if an user can delete a Service object. + + :param self: The Right which is to be deleted. + :param user_request: The user who requests deletion. + :return: True if deletion is granted, and a message. + """ + return user_request.has_perm('preferences.delete_service'), u"Vous n'avez pas le droit\ + de supprimer les services pour la page d'accueil" + + def can_view_all(user_request, *args, **kwargs): + """Check if an user can access to the list of every Service objects + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation message. + """ + + return user_request.has_perm('preferences.view_service'), u"Vous n'avez pas le droit\ + de voir les services pour la page d'accueil" + + def can_view(self, user_request, *args, **kwargs): + """Check if an user can view a Service object. + + :param self: The targeted Service. + :param user_request: The user who ask for viewing the target. + :return: A boolean telling if the acces is granted and an explanation + text + """ + return user_request.has_perm('preferences.view_service'), u"Vous n'avez pas le droit\ + de voir les services pour la page d'accueil" + def __str__(self): return str(self.name) -class AssoOption(models.Model): +class AssoOption(PreferencesModel): """Options générales de l'asso : siret, addresse, nom, etc""" PRETTY_NAME = "Options de l'association" @@ -147,6 +544,88 @@ class AssoOption(models.Model): blank=True, null=True ) + PAYMENT = ( + ('NONE', 'NONE'), + ('COMNPAY', 'COMNPAY'), + ) + payment = models.CharField(max_length=255, + choices=PAYMENT, + default='NONE', + ) + payment_id = models.CharField( + max_length=255, + default='', + ) + payment_pass = AESEncryptedField( + max_length=255, + null=True, + blank=True, + ) + description = models.TextField(default="") + + class Meta: + permissions = ( + ("view_assooption", "Peut voir les options de l'asso"), + ) + + def get_instance(*args, **kwargs): + return AssoOption.objects.get_or_create() + + def can_create(user_request, *args, **kwargs): + """Check if an user can create a AssoOption object. + + :param user_request: The user who wants to create an object. + :return: a message and a boolean which is True if the user can create. + """ + return user_request.has_perm('preferences.add_assooption'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant l'association" + + def can_edit(self, user_request, *args, **kwargs): + """Check if an user can edit a AssoOption object. + + :param self: The AssoOption which is to be edited. + :param user_request: The user who requests to edit self. + :return: a message and a boolean which is True if edition is granted. + """ + return user_request.has_perm('preferences.change_assooption'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant l'association" + + def can_delete(self, user_request, *args, **kwargs): + """Check if an user can delete a AssoOption object. + + :param self: The AssoOption which is to be deleted. + :param user_request: The user who requests deletion. + :return: True if deletion is granted, and a message. + """ + return user_request.has_perm('preferences.delete_assooption'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant l'association" + + def can_view_all(user_request, *args, **kwargs): + """Check if an user can access to the list of every AssoOption objects + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation message. + """ + return user_request.has_perm('preferences.view_assooption'), u"Vous n'avez pas le droit\ + de voir les préférences concernant l'association" + + def can_view(self, user_request, *args, **kwargs): + """Check if an user can view a AssoOption object. + + :param self: The targeted AssoOption. + :param user_request: The user who ask for viewing the target. + :return: A boolean telling if the acces is granted and an explanation + text + """ + return user_request.has_perm('preferences.view_assooption'), u"Vous n'avez pas le droit\ + de voir les préférences concernant l'association" + + +@receiver(post_save, sender=AssoOption) +def assooption_post_save(sender, **kwargs): + """Ecriture dans le cache""" + asso_pref = kwargs['instance'] + asso_pref.set_in_cache() class MailMessageOption(models.Model): @@ -155,3 +634,61 @@ class MailMessageOption(models.Model): welcome_mail_fr = models.TextField(default="") welcome_mail_en = models.TextField(default="") + + class Meta: + permissions = ( + ("view_mailmessageoption", "Peut voir les options de mail"), + ) + + def get_instance(*args, **kwargs): + return MailMessageOption.objects.get_or_create() + + def can_create(user_request, *args, **kwargs): + """Check if an user can create a MailMessageOption object. + + :param user_request: The user who wants to create an object. + :return: a message and a boolean which is True if the user can create. + """ + return user_request.has_perm('preferences.add_mailmessageoption'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant les mails" + + def can_edit(self, user_request, *args, **kwargs): + """Check if an user can edit a MailMessageOption object. + + :param self: The MailMessageOption which is to be edited. + :param user_request: The user who requests to edit self. + :return: a message and a boolean which is True if edition is granted. + """ + + return user_request.has_perm('preferences.change_mailmessageoption'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant les mails" + + def can_delete(self, user_request, *args, **kwargs): + """Check if an user can delete a AssoOption object. + + :param self: The AssoOption which is to be deleted. + :param user_request: The user who requests deletion. + :return: True if deletion is granted, and a message. + """ + return user_request.has_perm('preferences.delete_mailmessageoption'), u"Vous n'avez pas le droit\ + d'éditer les préférences concernant les mails" + + def can_view_all(user_request, *args, **kwargs): + """Check if an user can access to the list of every AssoOption objects + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation message. + """ + return user_request.has_perm('preferences.view_mailmessageoption'), u"Vous n'avez pas le droit\ + de voir les préférences concernant les mails" + + def can_view(self, user_request, *args, **kwargs): + """Check if an user can view a AssoOption object. + + :param self: The targeted AssoOption. + :param user_request: The user who ask for viewing the target. + :return: A boolean telling if the acces is granted and an explanation + text + """ + return user_request.has_perm('preferences.view_mailmessageoption'), u"Vous n'avez pas le droit\ + de voir les préférences concernant les mails" diff --git a/preferences/templates/preferences/aff_service.html b/preferences/templates/preferences/aff_service.html index e5f8aecf..b4d4894c 100644 --- a/preferences/templates/preferences/aff_service.html +++ b/preferences/templates/preferences/aff_service.html @@ -21,7 +21,7 @@ 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 %}
+ {% can_delete pl %} {% include 'buttons/suppr.html' with href='machines:del-portlist' id=pl.id %} + {% acl_end %} + {% can_edit pl %} {% include 'buttons/edit.html' with href='machines:edit-portlist' id=pl.id %} + {% acl_end %}
@@ -40,9 +40,9 @@ with this program; if not, write to the Free Software Foundation, Inc., diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index ad807155..67666bac 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -24,17 +24,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load acl %} {% block title %}Création et modification des préférences{% endblock %} {% block content %}

Préférences utilisateur

- {% if is_bureau %} - + Editer - {% endif %}

{{ service.description }} {{ service.image }} - {% if is_admin %} - {% include 'buttons/edit.html' with href='preferences:edit-services' id=service.id %} - {% endif %} + {% can_edit service%} + {% include 'buttons/edit.html' with href='preferences:edit-service' id=service.id %} + {% acl_end %} {% include 'buttons/history.html' with href='preferences:history' name='service' id=service.id %}
@@ -53,17 +52,29 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} - - + + + + + + + + + {% if useroptions.user_solde %} + + + + + + + {% endif %}
Creations d'users par tous{{ useroptions.all_can_create }}Creations d'adhérents par tous{{ useroptions.all_can_create_adherent }}Creations de clubs par tous{{ useroptions.all_can_create_club }}
Auto inscription{{ useroptions.self_adhesion }}
Solde maximum{{ useroptions.max_solde }}Montant minimal de rechargement en ligne{{ useroptions.min_online_payment }}

Préférences machines

- {% if is_bureau %} - + Editer - {% endif %}

@@ -77,16 +88,18 @@ with this program; if not, write to the Free Software Foundation, Inc., - + + + + +
Alias dns autorisé par utilisateur {{ machineoptions.max_lambdauser_aliases }} Support de l'ipv6{{ machineoptions.ipv6 }}{{ machineoptions.ipv6_mode }}
Creation de machines{{ machineoptions.create_machine }}

Préférences topologie

- {% if is_bureau %} - + Editer - {% endif %}

@@ -104,14 +117,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Préférences generales

- {% if is_bureau %} - + Editer - {% endif %}

-

+

@@ -134,15 +145,19 @@ with this program; if not, write to the Free Software Foundation, Inc., + + + + +
Nom du site web
Message global affiché sur le site {{ generaloptions.general_message }}Résumé des CGU{{ generaloptions.GTU_sum_up }}
CGU{{generaloptions.GTU}} +

Données de l'association

- {% if is_bureau %} - + Editer - {% endif %}

@@ -164,19 +179,22 @@ with this program; if not, write to the Free Software Foundation, Inc., - + - -
{{ assooptions.telephone }} Pseudo d'usage {{ assooptions.pseudo }}
Objet utilisateur de l'association {{ assooptions.utilisateur_asso }}
+
Moyen de paiement automatique{{ assooptions.payment }}Description de l'association{{ assooptions.description }}

Messages personalisé dans les mails

- {% if is_bureau %} - + Editer - {% endif %}

@@ -190,10 +208,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Liste des services page d'accueil

- {% if is_infra %} - Ajouter un service - Supprimer un ou plusieurs service - {% endif %} + {% can_create preferences.Service%} + Ajouter un service + {% acl_end %} + Supprimer un ou plusieurs service {% include "preferences/aff_service.html" with service_list=service_list %}

diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 02f006c1..055ac7e8 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -33,10 +33,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Edition des préférences

-
+ {% csrf_token %} {% massive_bootstrap_form options 'utilisateur_asso' %} -{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} +{% bootstrap_button "Modifier" button_type="submit" icon="star" %}


diff --git a/preferences/templates/preferences/preferences.html b/preferences/templates/preferences/preferences.html index 6184bfb3..16d7a74d 100644 --- a/preferences/templates/preferences/preferences.html +++ b/preferences/templates/preferences/preferences.html @@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if preferenceform %} {% bootstrap_form preferenceform %} {% endif %} - {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} + {% bootstrap_button action_name button_type="submit" icon="star" %}

diff --git a/preferences/urls.py b/preferences/urls.py index f10d25a0..3bc15275 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -28,6 +28,7 @@ from __future__ import unicode_literals from django.conf.urls import url from . import views +import re2o urlpatterns = [ @@ -61,17 +62,18 @@ urlpatterns = [ views.edit_options, name='edit-options' ), - url(r'^add_services/$', views.add_services, name='add-services'), + url(r'^add_service/$', views.add_service, name='add-service'), url( - r'^edit_services/(?P[0-9]+)$', - views.edit_services, - name='edit-services' + r'^edit_service/(?P[0-9]+)$', + views.edit_service, + name='edit-service' ), url(r'^del_services/$', views.del_services, name='del-services'), url( - r'^history/(?Pservice)/(?P[0-9]+)$', - views.history, - name='history' + r'^history/(?P\w+)/(?P[0-9]+)$', + re2o.views.history, + name='history', + kwargs={'application':'preferences'}, ), url(r'^$', views.display_options, name='display-options'), ] diff --git a/preferences/views.py b/preferences/views.py index 493c1d14..43befc72 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -42,6 +42,7 @@ from reversion.models import Version from reversion import revisions as reversion from re2o.views import form +from re2o.acl import can_create, can_edit, can_delete_set, can_view_all from .forms import ServiceForm, DelServiceForm from .models import Service, OptionalUser, OptionalMachine, AssoOption from .models import MailMessageOption, GeneralOption, OptionalTopologie @@ -50,7 +51,12 @@ from . import forms @login_required -@permission_required('cableur') +@can_view_all(OptionalUser) +@can_view_all(OptionalMachine) +@can_view_all(OptionalTopologie) +@can_view_all(GeneralOption) +@can_view_all(AssoOption) +@can_view_all(MailMessageOption) def display_options(request): """Vue pour affichage des options (en vrac) classé selon les models correspondants dans un tableau""" @@ -73,90 +79,90 @@ def display_options(request): @login_required -@permission_required('admin') def edit_options(request, section): """ Edition des préférences générales""" model = getattr(models, section, None) form_instance = getattr(forms, 'Edit' + section + 'Form', None) - if model and form: - options_instance, _created = model.objects.get_or_create() - options = form_instance( - request.POST or None, - instance=options_instance - ) - if options.is_valid(): - with transaction.atomic(), reversion.create_revision(): - options.save() - reversion.set_user(request.user) - reversion.set_comment( - "Champs modifié(s) : %s" % ', '.join( - field for field in options.changed_data - ) - ) - messages.success(request, "Préférences modifiées") - return redirect(reverse('preferences:display-options')) - return form( - {'options': options}, - 'preferences/edit_preferences.html', - request - ) - else: + if not (model or form_instance): messages.error(request, "Objet inconnu") return redirect(reverse('preferences:display-options')) + options_instance, _created = model.objects.get_or_create() + can, msg = options_instance.can_edit(request.user) + if not can: + messages.error(request, msg or "Vous ne pouvez pas éditer cette\ + option.") + return redirect(reverse('index')) + options = form_instance( + request.POST or None, + request.FILES or None, + instance=options_instance + ) + if options.is_valid(): + with transaction.atomic(), reversion.create_revision(): + options.save() + reversion.set_user(request.user) + reversion.set_comment( + "Champs modifié(s) : %s" % ', '.join( + field for field in options.changed_data + ) + ) + messages.success(request, "Préférences modifiées") + return redirect(reverse('preferences:display-options')) + return form( + {'options': options}, + 'preferences/edit_preferences.html', + request + ) + @login_required -@permission_required('admin') -def add_services(request): +@can_create(Service) +def add_service(request): """Ajout d'un service de la page d'accueil""" - services = ServiceForm(request.POST or None) - if services.is_valid(): + service = ServiceForm(request.POST or None) + if service.is_valid(): with transaction.atomic(), reversion.create_revision(): - services.save() + service.save() reversion.set_user(request.user) reversion.set_comment("Création") messages.success(request, "Ce service a été ajouté") return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': services}, + {'preferenceform': service, 'action_name' : 'Ajouter'}, 'preferences/preferences.html', request ) @login_required -@permission_required('admin') -def edit_services(request, servicesid): +@can_edit(Service) +def edit_service(request, service_instance, serviceid): """Edition des services affichés sur la page d'accueil""" - try: - services_instance = Service.objects.get(pk=servicesid) - except Service.DoesNotExist: - messages.error(request, u"Entrée inexistante") - return redirect(reverse('preferences:display-options')) - services = ServiceForm(request.POST or None, instance=services_instance) - if services.is_valid(): + service = ServiceForm(request.POST or None, instance=service_instance) + if service.is_valid(): with transaction.atomic(), reversion.create_revision(): - services.save() + service.save() reversion.set_user(request.user) reversion.set_comment( "Champs modifié(s) : %s" % ', '.join( - field for field in services.changed_data + field for field in service.changed_data ) ) messages.success(request, "Service modifié") return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': services}, + {'preferenceform': service, 'action_name' : 'Editer'}, 'preferences/preferences.html', request ) @login_required -@permission_required('admin') -def del_services(request): +@can_delete_set(Service) +def del_services(request, instances): """Suppression d'un service de la page d'accueil""" - services = DelServiceForm(request.POST or None) + services = DelServiceForm(request.POST or None, instances=instances) if services.is_valid(): services_dels = services.cleaned_data['services'] for services_del in services_dels: @@ -164,43 +170,13 @@ def del_services(request): with transaction.atomic(), reversion.create_revision(): services_del.delete() reversion.set_user(request.user) - messages.success(request, "Le services a été supprimée") + messages.success(request, "Le service a été supprimée") except ProtectedError: messages.error(request, "Erreur le service\ suivant %s ne peut être supprimé" % services_del) return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': services}, + {'preferenceform': services, 'action_name' : 'Supprimer'}, 'preferences/preferences.html', request ) - - -@login_required -@permission_required('cableur') -def history(request, object_name, object_id): - """Historique de creation et de modification d'un service affiché sur - la page d'accueil""" - if object_name == 'service': - try: - object_instance = Service.objects.get(pk=object_id) - except Service.DoesNotExist: - messages.error(request, "Service inexistant") - return redirect(reverse('preferences:display-options')) - options, _created = GeneralOption.objects.get_or_create() - pagination_number = options.pagination_number - reversions = Version.objects.get_for_object(object_instance) - paginator = Paginator(reversions, pagination_number) - page = request.GET.get('page') - try: - reversions = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - reversions = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - reversions = paginator.page(paginator.num_pages) - return render(request, 're2o/history.html', { - 'reversions': reversions, - 'object': object_instance - }) diff --git a/re2o/acl.py b/re2o/acl.py new file mode 100644 index 00000000..a4c0027c --- /dev/null +++ b/re2o/acl.py @@ -0,0 +1,248 @@ +# -*- mode: python; coding: utf-8 -*- +# 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. + +"""Handles ACL for re2o. + +Here are defined some decorators that can be used in views to handle ACL. +""" +from __future__ import unicode_literals + +import sys + +from django.contrib import messages +from django.shortcuts import redirect +from django.urls import reverse + +import cotisations +import logs +import machines +import preferences +import search +import topologie +import users + + +def can_create(model): + """Decorator to check if an user can create a model. + It assumes that a valid user exists in the request and that the model has a + method can_create(user) which returns true if the user can create this kind + of models. + """ + def decorator(view): + def wrapper(request, *args, **kwargs): + can, msg = model.can_create(request.user, *args, **kwargs) + if not can: + messages.error( + request, msg or "Vous ne pouvez pas accéder à ce menu") + return redirect(reverse('index')) + return view(request, *args, **kwargs) + return wrapper + return decorator + + +def can_edit(model, *field_list): + """Decorator to check if an user can edit a model. + It tries to get an instance of the model, using + `model.get_instance(*args, **kwargs)` and assumes that the model has a + method `can_edit(user)` which returns `true` if the user can edit this + kind of models. + """ + def decorator(view): + def wrapper(request, *args, **kwargs): + try: + instance = model.get_instance(*args, **kwargs) + except model.DoesNotExist: + messages.error(request, u"Entrée inexistante") + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + can, msg = instance.can_edit(request.user) + if not can: + messages.error( + request, msg or "Vous ne pouvez pas accéder à ce menu") + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + for field in field_list: + can_change = getattr(instance, 'can_change_' + field) + can, msg = can_change(request.user, *args, **kwargs) + if not can: + messages.error( + request, msg or "Vous ne pouvez pas accéder à ce menu") + return redirect(reverse('users:profil', + kwargs={'userid': str( + request.user.id)} + )) + return view(request, instance, *args, **kwargs) + return wrapper + return decorator + + +def can_change(model, *field_list): + """Decorator to check if an user can edit a field of a model class. + Difference with can_edit : take a class and not an instance + """ + def decorator(view): + def wrapper(request, *args, **kwargs): + for field in field_list: + can_change = getattr(model, 'can_change_' + field) + can, msg = can_change(request.user, *args, **kwargs) + if not can: + messages.error( + request, msg or "Vous ne pouvez pas accéder à ce menu") + return redirect(reverse('users:profil', + kwargs={'userid': str( + request.user.id)} + )) + return view(request, *args, **kwargs) + return wrapper + return decorator + + +def can_delete(model): + """Decorator to check if an user can delete a model. + It tries to get an instance of the model, using + `model.get_instance(*args, **kwargs)` and assumes that the model has a + method `can_delete(user)` which returns `true` if the user can delete this + kind of models. + """ + def decorator(view): + def wrapper(request, *args, **kwargs): + try: + instance = model.get_instance(*args, **kwargs) + except model.DoesNotExist: + messages.error(request, u"Entrée inexistante") + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + can, msg = instance.can_delete(request.user) + if not can: + messages.error( + request, msg or "Vous ne pouvez pas accéder à ce menu") + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + return view(request, instance, *args, **kwargs) + return wrapper + return decorator + + +def can_delete_set(model): + """Decorator which returns a list of detable models by request user. + If none of them, return an error""" + def decorator(view): + def wrapper(request, *args, **kwargs): + all_objects = model.objects.all() + instances_id = [] + for instance in all_objects: + can, msg = instance.can_delete(request.user) + if can: + instances_id.append(instance.id) + instances = model.objects.filter(id__in=instances_id) + if not instances: + messages.error(request, "Vous ne pouvez pas accéder à ce menu") + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + return view(request, instances, *args, **kwargs) + return wrapper + return decorator + + +def can_view(model): + """Decorator to check if an user can view a model. + It tries to get an instance of the model, using + `model.get_instance(*args, **kwargs)` and assumes that the model has a + method `can_view(user)` which returns `true` if the user can view this + kind of models. + """ + def decorator(view): + def wrapper(request, *args, **kwargs): + try: + instance = model.get_instance(*args, **kwargs) + except model.DoesNotExist: + messages.error(request, u"Entrée inexistante") + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + can, msg = instance.can_view(request.user) + if not can: + messages.error( + request, msg or "Vous ne pouvez pas accéder à ce menu") + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + return view(request, instance, *args, **kwargs) + return wrapper + return decorator + + +def can_view_all(model): + """Decorator to check if an user can view a class of model. + """ + def decorator(view): + def wrapper(request, *args, **kwargs): + can, msg = model.can_view_all(request.user) + if not can: + messages.error( + request, msg or "Vous ne pouvez pas accéder à ce menu") + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + return view(request, *args, **kwargs) + return wrapper + return decorator + + +def can_view_app(app_name): + """Decorator to check if an user can view an application. + """ + assert app_name in sys.modules.keys() + + def decorator(view): + def wrapper(request, *args, **kwargs): + app = sys.modules[app_name] + can, msg = app.can_view(request.user) + if can: + return view(request, *args, **kwargs) + messages.error(request, msg) + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + return wrapper + return decorator + + +def can_edit_history(view): + """Decorator to check if an user can edit history.""" + def wrapper(request, *args, **kwargs): + if request.user.has_perm('admin.change_logentry'): + return view(request, *args, **kwargs) + messages.error( + request, + "Vous ne pouvez pas éditer l'historique." + ) + return redirect(reverse('users:profil', + kwargs={'userid': str(request.user.id)} + )) + return wrapper diff --git a/re2o/context_processors.py b/re2o/context_processors.py index 0f9ea3fc..5a34e34b 100644 --- a/re2o/context_processors.py +++ b/re2o/context_processors.py @@ -31,37 +31,17 @@ from preferences.models import GeneralOption, OptionalMachine def context_user(request): """Fonction de context lorsqu'un user est logué (ou non), renvoie les infos sur l'user, la liste de ses droits, ses machines""" - general_options, _created = GeneralOption.objects.get_or_create() - machine_options, _created = OptionalMachine.objects.get_or_create() user = request.user - global_message = general_options.general_message + global_message = GeneralOption.get_cached_value('general_message') if global_message: messages.warning(request, global_message) if user.is_authenticated(): interfaces = user.user_interfaces() - is_cableur = user.is_cableur - is_bureau = user.is_bureau - is_bofh = user.is_bofh - is_trez = user.is_trez - is_infra = user.is_infra - is_admin = user.is_admin else: interfaces = None - is_cableur = False - is_bureau = False - is_bofh = False - is_trez = False - is_infra = False - is_admin = False return { 'request_user': user, - 'is_cableur': is_cableur, - 'is_bureau': is_bureau, - 'is_bofh': is_bofh, - 'is_trez': is_trez, - 'is_infra': is_infra, - 'is_admin': is_admin, 'interfaces': interfaces, - 'site_name': general_options.site_name, - 'ipv6_enabled': machine_options.ipv6, + 'site_name': GeneralOption.get_cached_value('site_name'), + 'ipv6_enabled': OptionalMachine.get_cached_value('ipv6'), } diff --git a/re2o/field_permissions.py b/re2o/field_permissions.py new file mode 100644 index 00000000..dc5466c4 --- /dev/null +++ b/re2o/field_permissions.py @@ -0,0 +1,79 @@ +from django.db import models +from django import forms +from functools import partial + + +class FieldPermissionModelMixin: + field_permissions = {} # {'field_name': callable} + FIELD_PERM_CODENAME = 'can_change_{model}_{name}' + FIELD_PERMISSION_GETTER = 'can_change_{name}' + FIELD_PERMISSION_MISSING_DEFAULT = True + + def has_field_perm(self, user, field): + if field in self.field_permissions: + checks = self.field_permissions[field] + if not isinstance(checks, (list, tuple)): + checks = [checks] + + else: + checks = [] + + # Consult the optional field-specific hook. + getter_name = self.FIELD_PERMISSION_GETTER.format(name=field) + if hasattr(self, getter_name): + checks.append(getattr(self, getter_name)) + + # Try to find a static permission for the field + else: + perm_label = self.FIELD_PERM_CODENAME.format(**{ + 'model': self._meta.model_name, + 'name': field, + }) + if perm_label in dict(self._meta.permissions): + checks.append(perm_label) + + # No requirements means no restrictions. + if not len(checks): + return self.FIELD_PERMISSION_MISSING_DEFAULT + + # Try to find a user setting that qualifies them for permission. + for perm in checks: + if callable(perm): + result, reason = perm(user_request=user) + if result is not None: + return result + else: + result = user.has_perm(perm) # Don't supply 'obj', or else infinite recursion. + if result: + return True + + # If no requirement can be met, then permission is denied. + return False + +class FieldPermissionModel(FieldPermissionModelMixin, models.Model): + class Meta: + abstract = True + + +class FieldPermissionFormMixin: + """ + Construit le formulaire et retire les champs interdits + """ + def __init__(self, *args, **kwargs): + user = kwargs.pop('user') + + super(FieldPermissionFormMixin, self).__init__(*args, **kwargs) + to_be_deleted = [] + for name in self.fields: + if not self.instance.has_field_perm(user, field=name): + to_be_deleted.append(name) + for name in to_be_deleted: + self.remove_unauthorized_field(name) + + def remove_unauthorized_field(self, name): + del self.fields[name] + + +class FieldPermissionForm(FieldPermissionFormMixin, forms.ModelForm): + pass + diff --git a/re2o/management/__init__.py b/re2o/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/re2o/management/commands/__init__.py b/re2o/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/re2o/management/commands/gen_contrib.py b/re2o/management/commands/gen_contrib.py new file mode 100644 index 00000000..d33b560d --- /dev/null +++ b/re2o/management/commands/gen_contrib.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# 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 © 2018 Matthieu Michelet +# Copyright © 2018 Gabriel Detraz +# +# 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. +""" +Write in a python file the list of all contributors sorted by number of commits. +This list is extracted from the FedeRez gitlab repository. +""" + +from django.core.management.base import BaseCommand, CommandError +import os + +class Command(BaseCommand): + help = 'Update contributors list' + + def handle(self, *args, **options): + contributeurs = [item.split('\t')[1] for item in os.popen("git shortlog -s -n").read().split("\n") if '\t' in item] + self.stdout.write(self.style.SUCCESS("Exportation Sucessfull")) + contrib_file = open("contributors.py", "w") + contrib_file.write("#!/usr/bin/env python3\n") + contrib_file.write("\n") + contrib_file.write("contributeurs = " + str(contributeurs)) diff --git a/re2o/settings.py b/re2o/settings.py index c342ef93..d1c27192 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -76,7 +76,8 @@ INSTALLED_APPS = ( 'preferences', 'logs', 'rest_framework', - 'reversion' + 'reversion', + 'api' ) + OPTIONNAL_APPS MIDDLEWARE_CLASSES = ( @@ -150,7 +151,7 @@ STATICFILES_DIRS = ( ), ) -MEDIA_ROOT = '/var/www/re2o/static' +MEDIA_ROOT = '/var/www/re2o/media' STATIC_URL = '/static/' diff --git a/re2o/settings_local.example.py b/re2o/settings_local.example.py index 967386bb..c541840f 100644 --- a/re2o/settings_local.example.py +++ b/re2o/settings_local.example.py @@ -26,6 +26,10 @@ SECRET_KEY = 'SUPER_SECRET_KEY' DB_PASSWORD = 'SUPER_SECRET_DB' +# AES key for secret key encryption length must be a multiple of 16 +AES_KEY = 'THE_AES_KEY' + + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False @@ -53,12 +57,12 @@ DATABASES = { } } -# Security settings -SECURE_CONTENT_TYPE_NOSNIFF = True -SECURE_BROWSER_XSS_FILTER = True -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True -CSRF_COOKIE_HTTPONLY = True +# Security settings, à activer une fois https en place +SECURE_CONTENT_TYPE_NOSNIFF = False +SECURE_BROWSER_XSS_FILTER = False +SESSION_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False +CSRF_COOKIE_HTTPONLY = False X_FRAME_OPTIONS = 'DENY' SESSION_COOKIE_AGE = 60 * 60 * 3 diff --git a/re2o/templates/re2o/about.html b/re2o/templates/re2o/about.html new file mode 100644 index 00000000..daec6d91 --- /dev/null +++ b/re2o/templates/re2o/about.html @@ -0,0 +1,62 @@ +{% extends "re2o/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 %} + +{% block title %}Historique{% endblock %} + +{% block content %} +

À propos de {{AssoName}}

+ {{ description }} +

À propos de Re2o

+

Re2o est un logiciel d'administration développé initialement au RézoMetz. Il +se veut indépendant de l'architecture du réseau considéré, de manière à être installable en +quelques clics. +C'est un logiciel totallement libre disponible sur le gitlab de FedeRez. +

+ +

Liste des contributeurs

+
+
+ +
    + {% for i in contrib_1 %} +
  • {{i}}
  • + {% endfor %} +
+
+
+
    + {% for i in contrib_2 %} +
  • {{i}}
  • + {%endfor%} +
+
+
+
+
+
+{% endblock %} + diff --git a/re2o/templates/re2o/index.html b/re2o/templates/re2o/index.html index 477c64e2..877faf10 100644 --- a/re2o/templates/re2o/index.html +++ b/re2o/templates/re2o/index.html @@ -29,7 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}Accueil{% endblock %} {% block content %} -

Bienvenue sur {{ site_name }} !

+

Bienvenue sur {{ request.get_host }} !

{% for service_list in services_urls %} diff --git a/re2o/templatetags/__init__.py b/re2o/templatetags/__init__.py index d9561b4b..5be3ef33 100644 --- a/re2o/templatetags/__init__.py +++ b/re2o/templatetags/__init__.py @@ -2,19 +2,19 @@ # 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 Maël Kervella -# +# # 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. diff --git a/re2o/templatetags/acl.py b/re2o/templatetags/acl.py new file mode 100644 index 00000000..c14fcbd6 --- /dev/null +++ b/re2o/templatetags/acl.py @@ -0,0 +1,424 @@ +# -*- mode: python; coding: utf-8 -*- +# 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 Maël Kervella +# +# 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. + +""" +Set of templatetags for using acl in templates: + - can_create (model) + - cannot_create (model) + - can_edit (instance) + - cannot_edit (instance) + +Some templatetags require a model to calculate the acl while others are need +an instance of a model (either Model.can_xxx or instance.can_xxx) + +**Parameters**: + model_name or instance - Either the model_name (if templatetag is based on + model) or an instantiated object (if templatetag is base on instance) + that needs to be checked for the current user + args - Any other argument that is interpreted as a python object and passed + to the acl function (can_xxx) + +**Usage**: + {% [arg1 [arg2 [...]]]%} +