diff --git a/api/acl.py b/api/acl.py index 0c7faae7..8c39aed0 100644 --- a/api/acl.py +++ b/api/acl.py @@ -1,9 +1,8 @@ -# -*- 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 © 2018 Maël Kervella +# 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 @@ -19,34 +18,41 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""api.acl +"""Defines the ACL for the whole API. -Here are defined some functions to check acl on the application. +Importing this module, creates the 'can view api' permission if not already +done. """ - from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import Permission +from django.utils.translation import ugettext_lazy as _ -# Creates the 'use_api' permission if not created -# The 'use_api' is a fake permission in the sense -# it is not associated with an existing model and -# this ensure the permission is created every tun -api_content_type, created = ContentType.objects.get_or_create( - app_label=settings.API_CONTENT_TYPE_APP_LABEL, - model=settings.API_CONTENT_TYPE_MODEL -) -if created: - api_content_type.save() -api_permission, created = Permission.objects.get_or_create( - name=settings.API_PERMISSION_NAME, - content_type=api_content_type, - codename=settings.API_PERMISSION_CODENAME -) -if created: - api_permission.save() +def _create_api_permission(): + """Creates the 'use_api' permission if not created. + + The 'use_api' is a fake permission in the sense it is not associated with an + existing model and this ensure the permission is created every time this file + is imported. + """ + api_content_type, created = ContentType.objects.get_or_create( + app_label=settings.API_CONTENT_TYPE_APP_LABEL, + model=settings.API_CONTENT_TYPE_MODEL + ) + if created: + api_content_type.save() + api_permission, created = Permission.objects.get_or_create( + name=settings.API_PERMISSION_NAME, + content_type=api_content_type, + codename=settings.API_PERMISSION_CODENAME + ) + if created: + api_permission.save() + + +_create_api_permission() def can_view(user): @@ -64,4 +70,4 @@ def can_view(user): 'codename': settings.API_PERMISSION_CODENAME } can = user.has_perm('%(app_label)s.%(codename)s' % kwargs) - return can, None if can else "Vous ne pouvez pas voir cette application." + return can, None if can else _("You cannot see this application.") diff --git a/api/authentication.py b/api/authentication.py index 4dc5a6f3..469c51f1 100644 --- a/api/authentication.py +++ b/api/authentication.py @@ -1,20 +1,43 @@ +# 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. + +"""Defines the authentication classes used in the API to authenticate a user. +""" + import datetime + from django.conf import settings from django.utils.translation import ugettext_lazy as _ from rest_framework.authentication import TokenAuthentication from rest_framework import exceptions class ExpiringTokenAuthentication(TokenAuthentication): + """Authenticate a user if the provided token is valid and not expired. + """ def authenticate_credentials(self, key): - model = self.get_model() - try: - token = model.objects.select_related('user').get(key=key) - except model.DoesNotExist: - raise exceptions.AuthenticationFailed(_('Invalid token.')) - - if not token.user.is_active: - raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + """See base class. Add the verification the token is not expired. + """ + base = super(ExpiringTokenAuthentication, self) + user, token = base.authenticate_credentials(key) + # Check that the genration time of the token is not too old token_duration = datetime.timedelta( seconds=settings.API_TOKEN_DURATION ) diff --git a/api/pagination.py b/api/pagination.py index 2fc0aaf7..20dcad6e 100644 --- a/api/pagination.py +++ b/api/pagination.py @@ -1,15 +1,57 @@ +# 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. + +"""Defines the pagination classes used in the API to paginate the results. +""" + from rest_framework import pagination class PageSizedPagination(pagination.PageNumberPagination): - """ - Pagination subclass to all to control the page size + """Provide the possibility to control the page size by using the + 'page_size' parameter. The value 'all' can be used for this parameter + to retrieve all the results in a single page. + + Attributes: + page_size_query_param: The string to look for in the parameters of + a query to get the page_size requested. + all_pages_strings: A set of strings that can be used in the query to + request all results in a single page. + max_page_size: The maximum number of results a page can output no + matter what is requested. """ page_size_query_param = 'page_size' all_pages_strings = ('all',) max_page_size = 10000 def get_page_size(self, request): + """Retrieve the size of the page according to the parameters of the + request. + + Args: + request: the request of the user + + Returns: + A integer between 0 and `max_page_size` that represent the size + of the page to use. + """ try: page_size_str = request.query_params[self.page_size_query_param] if page_size_str in self.all_pages_strings: diff --git a/api/permissions.py b/api/permissions.py index e04abdaf..53f06620 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -1,13 +1,61 @@ +# 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. + +"""Defines the permission classes used in the API. +""" + from rest_framework import permissions, exceptions + from re2o.acl import can_create, can_edit, can_delete, can_view_all from . import acl + def can_see_api(*_, **__): + """Check if a user can view the API. + + Returns: + A function that takes a user as an argument and returns + an ACL tuple that assert this user can see the API. + """ return lambda user: acl.can_view(user) def _get_param_in_view(view, param_name): + """Utility function to retrieve an attribute in a view passed in argument. + + Uses the result of `{view}.get_{param_name}()` if existing else uses the + value of `{view}.{param_name}` directly. + + Args: + view: The view where to look into. + param_name: The name of the attribute to look for. + + Returns: + The result of the getter function if found else the value of the + attribute itself. + + Raises: + AssertionError: None of the getter function or the attribute are + defined in the view. + """ assert hasattr(view, 'get_'+param_name) \ or getattr(view, param_name, None) is not None, ( 'cannot apply {} on a view that does not set ' @@ -24,15 +72,30 @@ def _get_param_in_view(view, param_name): class ACLPermission(permissions.BasePermission): - """ - Permission subclass for views that requires a specific model-based - permission or don't define a queryset + """A permission class used to check the ACL to validate the permissions + of a user. + + The view must define a `.get_perms_map()` or a `.perms_map` attribute. + See the wiki for the syntax of this attribute. """ def get_required_permissions(self, method, view): - """ - Given a list of models and an HTTP method, return the list - of acl functions that the user is required to verify. + """Build the list of permissions required for the request to be + accepted. + + Args: + method: The HTTP method name used for the request. + view: The view which is responding to the request. + + Returns: + The list of ACL functions to apply to a user in order to check + if he has the right permissions. + + Raises: + AssertionError: None of `.get_perms_map()` or `.perms_map` are + defined in the view. + rest_framework.exception.MethodNotAllowed: The requested method + is not allowed for this view. """ perms_map = _get_param_in_view(view, 'perms_map') @@ -42,6 +105,22 @@ class ACLPermission(permissions.BasePermission): return [can_see_api()] + list(perms_map[method]) def has_permission(self, request, view): + """Check that the user has the permissions to perform the request. + + Args: + request: The request performed. + view: The view which is responding to the request. + + Returns: + A boolean indicating if the user has the permission to + perform the request. + + Raises: + AssertionError: None of `.get_perms_map()` or `.perms_map` are + defined in the view. + rest_framework.exception.MethodNotAllowed: The requested method + is not allowed for this view. + """ # Workaround to ensure ACLPermissions are not applied # to the root view when using DefaultRouter. if getattr(view, '_ignore_model_permissions', False): @@ -54,19 +133,20 @@ class ACLPermission(permissions.BasePermission): return all(perm(request.user)[0] for perm in perms) - def has_object_permission(self, request, view, obj): - # Should never be called here but documentation - # requires to implement this function - return False - class AutodetectACLPermission(permissions.BasePermission): + """A permission class used to autodetect the ACL needed to validate the + permissions of a user based on the queryset of the view. + + The view must define a `.get_queryset()` or a `.queryset` attribute. + + Attributes: + perms_map: The mapping of each valid HTTP method to the required + model-based ACL permissions. + perms_obj_map: The mapping of each valid HTTP method to the required + object-based ACL permissions. """ - Permission subclass in charge of checking the ACL to determine - if a user can access the models. Autodetect which ACL are required - based on a queryset. Requires `.queryset` or `.get_queryset()` - to be defined in the view. - """ + perms_map = { 'GET': [can_see_api, lambda model: model.can_view_all], 'OPTIONS': [can_see_api, lambda model: model.can_view_all], @@ -87,9 +167,20 @@ class AutodetectACLPermission(permissions.BasePermission): } def get_required_permissions(self, method, model): - """ - Given a model and an HTTP method, return the list of acl - functions that the user is required to verify. + """Build the list of model-based permissions required for the + request to be accepted. + + Args: + method: The HTTP method name used for the request. + view: The view which is responding to the request. + + Returns: + The list of ACL functions to apply to a user in order to check + if he has the right permissions. + + Raises: + rest_framework.exception.MethodNotAllowed: The requested method + is not allowed for this view. """ if method not in self.perms_map: raise exceptions.MethodNotAllowed(method) @@ -97,9 +188,20 @@ class AutodetectACLPermission(permissions.BasePermission): return [perm(model) for perm in self.perms_map[method]] def get_required_object_permissions(self, method, obj): - """ - Given an object and an HTTP method, return the list of acl - functions that the user is required to verify. + """Build the list of object-based permissions required for the + request to be accepted. + + Args: + method: The HTTP method name used for the request. + view: The view which is responding to the request. + + Returns: + The list of ACL functions to apply to a user in order to check + if he has the right permissions. + + Raises: + rest_framework.exception.MethodNotAllowed: The requested method + is not allowed for this view. """ if method not in self.perms_obj_map: raise exceptions.MethodNotAllowed(method) @@ -107,13 +209,26 @@ class AutodetectACLPermission(permissions.BasePermission): return [perm(obj) for perm in self.perms_obj_map[method]] def _queryset(self, view): - """ - Return the queryset associated with view and raise an error - is there is none. - """ return _get_param_in_view(view, 'queryset') def has_permission(self, request, view): + """Check that the user has the model-based permissions to perform + the request. + + Args: + request: The request performed. + view: The view which is responding to the request. + + Returns: + A boolean indicating if the user has the permission to + perform the request. + + Raises: + AssertionError: None of `.get_queryset()` or `.queryset` are + defined in the view. + rest_framework.exception.MethodNotAllowed: The requested method + is not allowed for this view. + """ # Workaround to ensure ACLPermissions are not applied # to the root view when using DefaultRouter. if getattr(view, '_ignore_model_permissions', False): @@ -128,8 +243,22 @@ class AutodetectACLPermission(permissions.BasePermission): return all(perm(request.user)[0] for perm in perms) def has_object_permission(self, request, view, obj): + """Check that the user has the object-based permissions to perform + the request. + + Args: + request: The request performed. + view: The view which is responding to the request. + + Returns: + A boolean indicating if the user has the permission to + perform the request. + + Raises: + rest_framework.exception.MethodNotAllowed: The requested method + is not allowed for this view. + """ # authentication checks have already executed via has_permission - queryset = self._queryset(view) user = request.user perms = self.get_required_object_permissions(request.method, obj) diff --git a/api/routers.py b/api/routers.py index cea69690..fcfb5077 100644 --- a/api/routers.py +++ b/api/routers.py @@ -2,7 +2,7 @@ # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # -# Copyright © 2018 Mael Kervella +# 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 @@ -17,12 +17,12 @@ # 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.routers -Definition of the custom routers to generate the URLs of the API +"""Defines the custom routers to generate the URLs of the API. """ from collections import OrderedDict + from django.conf.urls import url, include from django.core.urlresolvers import NoReverseMatch from rest_framework import views @@ -32,32 +32,60 @@ from rest_framework.reverse import reverse from rest_framework.schemas import SchemaGenerator from rest_framework.settings import api_settings + class AllViewsRouter(DefaultRouter): + """A router that can register both viewsets and views and generates + a full API root page with all the generated URLs. + """ + def __init__(self, *args, **kwargs): self.view_registry = [] super(AllViewsRouter, self).__init__(*args, **kwargs) def register_viewset(self, *args, **kwargs): - """ - Register a viewset in the router - Alias of `register` for convenience + """Register a viewset in the router. Alias of `register` for + convenience. + + See `register` in the base class for details. """ return self.register(*args, **kwargs) def register_view(self, pattern, view, name=None): - """ - Register a view in the router + """Register a view in the router. + + Args: + pattern: The URL pattern to use for this view. + view: The class-based view to register. + name: An optional name for the route generated. Defaults is + based on the pattern last section (delimited by '/'). """ if name is None: name = self.get_default_name(pattern) self.view_registry.append((pattern, view, name)) def get_default_name(self, pattern): + """Returns the name to use for the route if none was specified. + + Args: + pattern: The pattern for this route. + + Returns: + The name to use for this route. + """ return pattern.split('/')[-1] def get_api_root_view(self, schema_urls=None): - """ - Return a view to use as the API root. + """Create a class-based view to use as the API root. + + Highly inspired by the base class. See details on the implementation + in the base class. The only difference is that registered view URLs + are added after the registered viewset URLs on this root API page. + + Args: + schema_urls: A schema to use for the URLs. + + Returns: + The view to use to display the root API page. """ api_root_dict = OrderedDict() list_name = self.routes[0].name @@ -115,6 +143,12 @@ class AllViewsRouter(DefaultRouter): return APIRoot.as_view() def get_urls(self): + """Builds the list of URLs to register. + + Returns: + A list of the URLs generated based on the viewsets registered + followed by the URLs generated based on the views registered. + """ urls = super(AllViewsRouter, self).get_urls() for pattern, view, name in self.view_registry: diff --git a/api/serializers.py b/api/serializers.py index 9f4b89ed..a1e73091 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -2,7 +2,7 @@ # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # -# Copyright © 2018 Mael Kervella +# 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 @@ -18,8 +18,7 @@ # 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 +"""Defines the serializers of the API """ from rest_framework import serializers @@ -31,12 +30,15 @@ import topologie.models as topologie import users.models as users +# The namespace used for the API. It must match the namespace used in the +# urlpatterns to include the API URLs. API_NAMESPACE = 'api' class NamespacedHRField(serializers.HyperlinkedRelatedField): - """ A HyperlinkedRelatedField subclass to automatically prefix - view names with a namespace """ + """A `rest_framework.serializers.HyperlinkedRelatedField` subclass to + automatically prefix view names with the API namespace. + """ def __init__(self, view_name=None, **kwargs): if view_name is not None: view_name = '%s:%s' % (API_NAMESPACE, view_name) @@ -44,8 +46,9 @@ class NamespacedHRField(serializers.HyperlinkedRelatedField): class NamespacedHIField(serializers.HyperlinkedIdentityField): - """ A HyperlinkedIdentityField subclass to automatically prefix - view names with a namespace """ + """A `rest_framework.serializers.HyperlinkedIdentityField` subclass to + automatically prefix view names with teh API namespace. + """ def __init__(self, view_name=None, **kwargs): if view_name is not None: view_name = '%s:%s' % (API_NAMESPACE, view_name) @@ -53,16 +56,19 @@ class NamespacedHIField(serializers.HyperlinkedIdentityField): class NamespacedHMSerializer(serializers.HyperlinkedModelSerializer): - """ A HyperlinkedModelSerializer subclass to use `NamespacedHRField` as - field and automatically prefix view names with a namespace """ + """A `rest_framework.serializers.HyperlinkedModelSerializer` subclass to + automatically prefix view names with the API namespace. + """ serializer_related_field = NamespacedHRField serializer_url_field = NamespacedHIField -# COTISATIONS APP +# COTISATIONS class FactureSerializer(NamespacedHMSerializer): + """Serialize `cotisations.models.Facture` objects. + """ class Meta: model = cotisations.Facture fields = ('user', 'paiement', 'banque', 'cheque', 'date', 'valid', @@ -70,6 +76,8 @@ class FactureSerializer(NamespacedHMSerializer): class VenteSerializer(NamespacedHMSerializer): + """Serialize `cotisations.models.Vente` objects. + """ class Meta: model = cotisations.Vente fields = ('facture', 'number', 'name', 'prix', 'duration', @@ -77,6 +85,8 @@ class VenteSerializer(NamespacedHMSerializer): class ArticleSerializer(NamespacedHMSerializer): + """Serialize `cotisations.models.Article` objects. + """ class Meta: model = cotisations.Article fields = ('name', 'prix', 'duration', 'type_user', @@ -84,40 +94,52 @@ class ArticleSerializer(NamespacedHMSerializer): class BanqueSerializer(NamespacedHMSerializer): + """Serialize `cotisations.models.Banque` objects. + """ class Meta: model = cotisations.Banque fields = ('name', 'api_url') class PaiementSerializer(NamespacedHMSerializer): + """Serialize `cotisations.models.Paiement` objects. + """ class Meta: model = cotisations.Paiement fields = ('moyen', 'type_paiement', 'api_url') class CotisationSerializer(NamespacedHMSerializer): + """Serialize `cotisations.models.Cotisation` objects. + """ class Meta: model = cotisations.Cotisation fields = ('vente', 'type_cotisation', 'date_start', 'date_end', 'api_url') -# MACHINES APP +# MACHINES class MachineSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Machine` objects. + """ class Meta: model = machines.Machine fields = ('user', 'name', 'active', 'api_url') class MachineTypeSerializer(NamespacedHMSerializer): + """Serialize `machines.models.MachineType` objects. + """ class Meta: model = machines.MachineType fields = ('type', 'ip_type', 'api_url') class IpTypeSerializer(NamespacedHMSerializer): + """Serialize `machines.models.IpType` objects. + """ class Meta: model = machines.IpType fields = ('type', 'extension', 'need_infra', 'domaine_ip_start', @@ -126,12 +148,16 @@ class IpTypeSerializer(NamespacedHMSerializer): class VlanSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Vlan` objects. + """ class Meta: model = machines.Vlan fields = ('vlan_id', 'name', 'comment', 'api_url') class NasSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Nas` objects. + """ class Meta: model = machines.Nas fields = ('name', 'nas_type', 'machine_type', 'port_access_mode', @@ -139,6 +165,8 @@ class NasSerializer(NamespacedHMSerializer): class SOASerializer(NamespacedHMSerializer): + """Serialize `machines.models.SOA` objects. + """ class Meta: model = machines.SOA fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl', @@ -146,6 +174,8 @@ class SOASerializer(NamespacedHMSerializer): class ExtensionSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Extension` objects. + """ class Meta: model = machines.Extension fields = ('name', 'need_infra', 'origin', 'origin_v6', 'soa', @@ -153,24 +183,32 @@ class ExtensionSerializer(NamespacedHMSerializer): class MxSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Mx` objects. + """ class Meta: model = machines.Mx fields = ('zone', 'priority', 'name', 'api_url') class NsSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Ns` objects. + """ class Meta: model = machines.Ns fields = ('zone', 'ns', 'api_url') class TxtSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Txt` objects. + """ class Meta: model = machines.Txt fields = ('zone', 'field1', 'field2', 'api_url') class SrvSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Srv` objects. + """ class Meta: model = machines.Srv fields = ('service', 'protocole', 'extension', 'ttl', 'priority', @@ -178,6 +216,8 @@ class SrvSerializer(NamespacedHMSerializer): class InterfaceSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Interface` objects. + """ mac_address = serializers.CharField() active = serializers.BooleanField(source='is_active') @@ -188,12 +228,16 @@ class InterfaceSerializer(NamespacedHMSerializer): class Ipv6ListSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Ipv6List` objects. + """ class Meta: model = machines.Ipv6List fields = ('ipv6', 'interface', 'slaac_ip', 'api_url') class DomainSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Domain` objects. + """ class Meta: model = machines.Domain fields = ('interface_parent', 'name', 'extension', 'cname', @@ -201,12 +245,16 @@ class DomainSerializer(NamespacedHMSerializer): class IpListSerializer(NamespacedHMSerializer): + """Serialize `machines.models.IpList` objects. + """ class Meta: model = machines.IpList fields = ('ipv4', 'ip_type', 'need_infra', 'api_url') class ServiceSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Service` objects. + """ class Meta: model = machines.Service fields = ('service_type', 'min_time_regen', 'regular_time_regen', @@ -214,6 +262,8 @@ class ServiceSerializer(NamespacedHMSerializer): class ServiceLinkSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Service_link` objects. + """ class Meta: model = machines.Service_link fields = ('service', 'server', 'last_regen', 'asked_regen', @@ -224,6 +274,8 @@ class ServiceLinkSerializer(NamespacedHMSerializer): class OuverturePortListSerializer(NamespacedHMSerializer): + """Serialize `machines.models.OuverturePortList` objects. + """ class Meta: model = machines.OuverturePortList fields = ('name', 'tcp_ports_in', 'udp_ports_in', 'tcp_ports_out', @@ -231,15 +283,19 @@ class OuverturePortListSerializer(NamespacedHMSerializer): class OuverturePortSerializer(NamespacedHMSerializer): + """Serialize `machines.models.OuverturePort` objects. + """ class Meta: model = machines.OuverturePort fields = ('begin', 'end', 'port_list', 'protocole', 'io', 'api_url') -# PREFERENCES APP +# PREFERENCES class OptionalUserSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.OptionalUser` objects. + """ tel_mandatory = serializers.BooleanField(source='is_tel_mandatory') class Meta: @@ -250,6 +306,8 @@ class OptionalUserSerializer(NamespacedHMSerializer): class OptionalMachineSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.OptionalMachine` objects. + """ class Meta: model = preferences.OptionalMachine fields = ('password_machine', 'max_lambdauser_interfaces', @@ -258,6 +316,8 @@ class OptionalMachineSerializer(NamespacedHMSerializer): class OptionalTopologieSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.OptionalTopologie` objects. + """ class Meta: model = preferences.OptionalTopologie fields = ('radius_general_policy', 'vlan_decision_ok', @@ -265,6 +325,8 @@ class OptionalTopologieSerializer(NamespacedHMSerializer): class GeneralOptionSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.GeneralOption` objects. + """ class Meta: model = preferences.GeneralOption fields = ('general_message', 'search_display_page', @@ -274,12 +336,16 @@ class GeneralOptionSerializer(NamespacedHMSerializer): class ServiceSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.Service` objects. + """ class Meta: model = preferences.Service fields = ('name', 'url', 'description', 'image', 'api_url') class AssoOptionSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.AssoOption` objects. + """ class Meta: model = preferences.AssoOption fields = ('name', 'siret', 'adresse1', 'adresse2', 'contact', @@ -288,22 +354,28 @@ class AssoOptionSerializer(NamespacedHMSerializer): class HomeOptionSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.HomeOption` objects. + """ class Meta: model = preferences.HomeOption fields = ('facebook_url', 'twitter_url', 'twitter_account_name') class MailMessageOptionSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.MailMessageOption` objects. + """ class Meta: model = preferences.MailMessageOption fields = ('welcome_mail_fr', 'welcome_mail_en') -# TOPOLOGIE APP +# TOPOLOGIE class StackSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Stack` objects + """ class Meta: model = topologie.Stack fields = ('name', 'stack_id', 'details', 'member_id_min', @@ -311,12 +383,16 @@ class StackSerializer(NamespacedHMSerializer): class AccessPointSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.AccessPoint` objects + """ class Meta: model = topologie.AccessPoint fields = ('user', 'name', 'active', 'location', 'api_url') class SwitchSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Switch` objects + """ port_amount = serializers.IntegerField(source='number') class Meta: model = topologie.Switch @@ -325,30 +401,40 @@ class SwitchSerializer(NamespacedHMSerializer): class ModelSwitchSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.ModelSwitch` objects + """ class Meta: model = topologie.ModelSwitch fields = ('reference', 'constructor', 'api_url') class ConstructorSwitchSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.ConstructorSwitch` objects + """ class Meta: model = topologie.ConstructorSwitch fields = ('name', 'api_url') class SwitchBaySerializer(NamespacedHMSerializer): + """Serialize `topologie.models.SwitchBay` objects + """ class Meta: model = topologie.SwitchBay fields = ('name', 'building', 'info', 'api_url') class BuildingSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Building` objects + """ class Meta: model = topologie.Building fields = ('name', 'api_url') class SwitchPortSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Port` objects + """ class Meta: model = topologie.Port fields = ('switch', 'port', 'room', 'machine_interface', 'related', @@ -360,15 +446,19 @@ class SwitchPortSerializer(NamespacedHMSerializer): class RoomSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Room` objects + """ class Meta: model = topologie.Room fields = ('name', 'details', 'api_url') -# USERS APP +# USERS class UserSerializer(NamespacedHMSerializer): + """Serialize `users.models.User` objects. + """ access = serializers.BooleanField(source='has_access') uid = serializers.IntegerField(source='uid_number') @@ -383,6 +473,8 @@ class UserSerializer(NamespacedHMSerializer): class ClubSerializer(NamespacedHMSerializer): + """Serialize `users.models.Club` objects. + """ name = serializers.CharField(source='surname') access = serializers.BooleanField(source='has_access') uid = serializers.IntegerField(source='uid_number') @@ -399,6 +491,8 @@ class ClubSerializer(NamespacedHMSerializer): class AdherentSerializer(NamespacedHMSerializer): + """Serialize `users.models.Adherent` objects. + """ access = serializers.BooleanField(source='has_access') uid = serializers.IntegerField(source='uid_number') @@ -413,24 +507,32 @@ class AdherentSerializer(NamespacedHMSerializer): class ServiceUserSerializer(NamespacedHMSerializer): + """Serialize `users.models.ServiceUser` objects. + """ class Meta: model = users.ServiceUser fields = ('pseudo', 'access_group', 'comment', 'api_url') class SchoolSerializer(NamespacedHMSerializer): + """Serialize `users.models.School` objects. + """ class Meta: model = users.School fields = ('name', 'api_url') class ListRightSerializer(NamespacedHMSerializer): + """Serialize `users.models.ListRight` objects. + """ class Meta: model = users.ListRight fields = ('unix_name', 'gid', 'critical', 'details', 'api_url') class ShellSerializer(NamespacedHMSerializer): + """Serialize `users.models.ListShell` objects. + """ class Meta: model = users.ListShell fields = ('shell', 'api_url') @@ -440,6 +542,8 @@ class ShellSerializer(NamespacedHMSerializer): class BanSerializer(NamespacedHMSerializer): + """Serialize `users.models.Ban` objects. + """ active = serializers.BooleanField(source='is_active') class Meta: @@ -449,6 +553,8 @@ class BanSerializer(NamespacedHMSerializer): class WhitelistSerializer(NamespacedHMSerializer): + """Serialize `users.models.Whitelist` objects. + """ active = serializers.BooleanField(source='is_active') class Meta: @@ -456,10 +562,12 @@ class WhitelistSerializer(NamespacedHMSerializer): fields = ('user', 'raison', 'date_start', 'date_end', 'active', 'api_url') -# Services +# SERVICE REGEN class ServiceRegenSerializer(NamespacedHMSerializer): + """Serialize the data about the services to regen. + """ hostname = serializers.CharField(source='server.domain.name', read_only=True) service_name = serializers.CharField(source='service.service_type', read_only=True) need_regen = serializers.BooleanField() @@ -476,6 +584,9 @@ class ServiceRegenSerializer(NamespacedHMSerializer): class HostMacIpSerializer(serializers.ModelSerializer): + """Serialize the data about the hostname-ipv4-mac address association + to build the DHCP lease files. + """ hostname = serializers.CharField(source='domain.name', read_only=True) extension = serializers.CharField(source='domain.extension.name', read_only=True) mac_address = serializers.CharField(read_only=True) @@ -490,22 +601,34 @@ class HostMacIpSerializer(serializers.ModelSerializer): class SOARecordSerializer(SOASerializer): + """Serialize `machines.models.SOA` objects with the data needed to + generate a SOA DNS record. + """ class Meta: model = machines.SOA fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl') class OriginV4RecordSerializer(IpListSerializer): + """Serialize `machines.models.IpList` objects with the data needed to + generate an IPv4 Origin DNS record. + """ class Meta(IpListSerializer.Meta): fields = ('ipv4',) class OriginV6RecordSerializer(Ipv6ListSerializer): + """Serialize `machines.models.Ipv6List` objects with the data needed to + generate an IPv6 Origin DNS record. + """ class Meta(Ipv6ListSerializer.Meta): fields = ('ipv6',) class NSRecordSerializer(NsSerializer): + """Serialize `machines.models.Ns` objects with the data needed to + generate a NS DNS record. + """ target = serializers.CharField(source='ns.name', read_only=True) class Meta(NsSerializer.Meta): @@ -513,6 +636,9 @@ class NSRecordSerializer(NsSerializer): class MXRecordSerializer(MxSerializer): + """Serialize `machines.models.Mx` objects with the data needed to + generate a MX DNS record. + """ target = serializers.CharField(source='name.name', read_only=True) class Meta(MxSerializer.Meta): @@ -520,11 +646,17 @@ class MXRecordSerializer(MxSerializer): class TXTRecordSerializer(TxtSerializer): + """Serialize `machines.models.Txt` objects with the data needed to + generate a TXT DNS record. + """ class Meta(TxtSerializer.Meta): fields = ('field1', 'field2') class SRVRecordSerializer(SrvSerializer): + """Serialize `machines.models.Srv` objects with the data needed to + generate a SRV DNS record. + """ target = serializers.CharField(source='target.name', read_only=True) class Meta(SrvSerializer.Meta): @@ -532,6 +664,9 @@ class SRVRecordSerializer(SrvSerializer): class ARecordSerializer(serializers.ModelSerializer): + """Serialize `machines.models.Interface` objects with the data needed to + generate a A DNS record. + """ hostname = serializers.CharField(source='domain.name', read_only=True) ipv4 = serializers.CharField(source='ipv4.ipv4', read_only=True) @@ -541,6 +676,9 @@ class ARecordSerializer(serializers.ModelSerializer): class AAAARecordSerializer(serializers.ModelSerializer): + """Serialize `machines.models.Interface` objects with the data needed to + generate a AAAA DNS record. + """ hostname = serializers.CharField(source='domain.name', read_only=True) ipv6 = Ipv6ListSerializer(many=True, read_only=True) @@ -550,6 +688,9 @@ class AAAARecordSerializer(serializers.ModelSerializer): class CNAMERecordSerializer(serializers.ModelSerializer): + """Serialize `machines.models.Domain` objects with the data needed to + generate a CNAME DNS record. + """ alias = serializers.CharField(source='cname.name', read_only=True) hostname = serializers.CharField(source='name', read_only=True) @@ -559,6 +700,8 @@ class CNAMERecordSerializer(serializers.ModelSerializer): class DNSZonesSerializer(serializers.ModelSerializer): + """Serialize the data about DNS Zones. + """ soa = SOARecordSerializer() ns_records = NSRecordSerializer(many=True, source='ns_set') originv4 = OriginV4RecordSerializer(source='origin') @@ -577,14 +720,18 @@ class DNSZonesSerializer(serializers.ModelSerializer): 'aaaa_records', 'cname_records') -# Mailing +# MAILING class MailingMemberSerializer(UserSerializer): + """Serialize the data about a mailing member. + """ class Meta(UserSerializer.Meta): fields = ('name', 'pseudo', 'email') class MailingSerializer(ClubSerializer): + """Serialize the data about a mailing. + """ members = MailingMemberSerializer(many=True) admins = MailingMemberSerializer(source='administrators', many=True) diff --git a/api/settings.py b/api/settings.py index cd19594e..f8171638 100644 --- a/api/settings.py +++ b/api/settings.py @@ -1,11 +1,8 @@ -# -*- 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 +# 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 @@ -21,8 +18,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""api.settings -Django settings specific to the API. +"""Settings specific to the API. """ # RestFramework config for API @@ -49,4 +45,6 @@ API_PERMISSION_CODENAME = 'use_api' API_APPS = ( 'rest_framework.authtoken', ) + +# The expiration time for an authentication token API_TOKEN_DURATION = 86400 # 24 hours diff --git a/api/tests.py b/api/tests.py index aa4747eb..24545864 100644 --- a/api/tests.py +++ b/api/tests.py @@ -2,9 +2,7 @@ # 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 +# 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 @@ -19,8 +17,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. -"""api.tests -The tests for the API module. +"""Defines the test suite for the API """ import json @@ -34,13 +31,25 @@ import users.models as users class APIEndpointsTestCase(APITestCase): - # URLs that don't require to be authenticated + """Test case to test that all endpoints are reachable with respects to + authentication and permission checks. + + Attributes: + no_auth_endpoints: A list of endpoints that should be reachable + without authentication. + auth_no_perm_endpoints: A list of endpoints that should be reachable + when being authenticated but without permissions. + auth_perm_endpoints: A list of endpoints that should be reachable + when being authenticated and having the correct permissions. + stduser: A standard user with no permission used for the tests and + initialized at the beggining of this test case. + superuser: A superuser (with all permissions) used for the tests and + initialized at the beggining of this test case. + """ no_auth_endpoints = [ '/api/' ] - # URLs that require to be authenticated and have no special permissions auth_no_perm_endpoints = [] - # URLs that require to be authenticated and have special permissions auth_perm_endpoints = [ '/api/cotisations/articles/', # '/api/cotisations/articles//', @@ -160,49 +169,62 @@ class APIEndpointsTestCase(APITestCase): cls.superuser.delete() super().tearDownClass() - def check_responses_code(self, urls, expected_code, formats=[None], + def check_responses_code(self, urls, expected_code, formats=None, assert_more=None): - """ - Utility function to test if a list of urls answer an expected code + """Utility function to test if a list of urls answer an expected code. - :param urls: (list) The list of urls to test - :param expected_code: (int) The HTTP return code expected - :param formats: (list) The list of formats to use for the request - (Default: [None]) - :param assert_more: (func) A function to assert more specific data - in the same test. It is evaluated with the responsem object, the - url and the format used. + Args: + urls: The list of urls to test + expected_code: The HTTP return code expected + formats: The list of formats to use for the request. Default is to + only test `None` format. + assert_more: An optional function to assert more specific data in + the same test. The response object, the url and the format + used are passed as arguments. + + Raises: + AssertionError: The response got did not have the expected status + code. + Any exception raised in the evalutation of `assert_more`. """ + if formats is None: + formats = [None] for url in urls: for format in formats: with self.subTest(url=url, format=format): response = self.client.get(url, format=format) assert response.status_code == expected_code - if assert_more: + if assert_more is not None: assert_more(response, url, format) def test_no_auth_endpoints_with_no_auth(self): - """ - Test that every endpoint that does not require to be authenticated, - returns a Ok (200) response when not authenticated. + """Tests that every endpoint that does not require to be + authenticated, returns a Ok (200) response when not authenticated. + + Raises: + AssertionError: An endpoint did not have a 200 status code. """ urls = [endpoint.replace('', '1') for endpoint in self.no_auth_endpoints] self.check_responses_code(urls, codes.ok) def test_auth_endpoints_with_no_auth(self): - """ - Test that every endpoint that does require to be authenticated, + """Tests that every endpoint that does require to be authenticated, returns a Unauthorized (401) response when not authenticated. + + Raises: + AssertionError: An endpoint did not have a 401 status code. """ urls = [endpoint.replace('', '1') for endpoint in \ self.auth_no_perm_endpoints + self.auth_perm_endpoints] self.check_responses_code(urls, codes.unauthorized) def test_no_auth_endpoints_with_auth(self): - """ - Test that every endpoint that does not require to be authenticated, - returns a Ok (200) response when authenticated. + """Tests that every endpoint that does not require to be + authenticated, returns a Ok (200) response when authenticated. + + Raises: + AssertionError: An endpoint did not have a 200 status code. """ self.client.force_authenticate(user=self.stduser) urls = [endpoint.replace('', '1') @@ -210,10 +232,12 @@ class APIEndpointsTestCase(APITestCase): self.check_responses_code(urls, codes.ok) def test_auth_no_perm_endpoints_with_auth_and_no_perm(self): - """ - Test that every endpoint that does require to be authenticated and - no special permissions, returns a Ok (200) response when - authenticated but without permissions. + """Tests that every endpoint that does require to be authenticated and + no special permissions, returns a Ok (200) response when authenticated + but without permissions. + + Raises: + AssertionError: An endpoint did not have a 200 status code. """ self.client.force_authenticate(user=self.stduser) urls = [endpoint.replace('', '1') @@ -221,10 +245,12 @@ class APIEndpointsTestCase(APITestCase): self.check_responses_code(urls, codes.ok) def test_auth_perm_endpoints_with_auth_and_no_perm(self): - """ - Test that every endpoint that does require to be authenticated and + """Tests that every endpoint that does require to be authenticated and special permissions, returns a Forbidden (403) response when authenticated but without permissions. + + Raises: + AssertionError: An endpoint did not have a 403 status code. """ self.client.force_authenticate(user=self.stduser) urls = [endpoint.replace('', '1') @@ -232,9 +258,11 @@ class APIEndpointsTestCase(APITestCase): self.check_responses_code(urls, codes.forbidden) def test_auth_endpoints_with_auth_and_perm(self): - """ - Test that every endpoint that does require to be authenticated, - returns a Ok (200) response when authenticated with all permissions + """Tests that every endpoint that does require to be authenticated, + returns a Ok (200) response when authenticated with all permissions. + + Raises: + AssertionError: An endpoint did not have a 200 status code. """ self.client.force_authenticate(user=self.superuser) urls = [endpoint.replace('', '1') for endpoint \ @@ -242,10 +270,12 @@ class APIEndpointsTestCase(APITestCase): self.check_responses_code(urls, codes.ok) def test_endpoints_not_found(self): - """ - Test that every endpoint that uses a primary key parameter, + """Tests that every endpoint that uses a primary key parameter, returns a Not Found (404) response when queried with non-existing - primary key + primary key. + + Raises: + AssertionError: An endpoint did not have a 404 status code. """ self.client.force_authenticate(user=self.superuser) # Select only the URLs with '' and replace it with '42' @@ -255,9 +285,12 @@ class APIEndpointsTestCase(APITestCase): self.check_responses_code(urls, codes.not_found) def test_formats(self): - """ - Test that every endpoint returns a Ok (200) response when using - different formats. Also checks that 'json' format returns a valid json + """Tests that every endpoint returns a Ok (200) response when using + different formats. Also checks that 'json' format returns a valid + JSON object. + + Raises: + AssertionError: An endpoint did not have a 200 status code. """ self.client.force_authenticate(user=self.superuser) @@ -275,6 +308,14 @@ class APIEndpointsTestCase(APITestCase): assert_more=assert_more) class APIPaginationTestCase(APITestCase): + """Test case to check that the pagination is used on all endpoints that + should use it. + + Attributes: + endpoints: A list of endpoints that should use the pagination. + superuser: A superuser used in the tests to access the endpoints. + """ + endpoints = [ '/api/cotisations/articles/', '/api/cotisations/banques/', @@ -338,8 +379,12 @@ class APIPaginationTestCase(APITestCase): super().tearDownClass() def test_pagination(self): - """ - Test that every endpoint is using the pagination correctly + """Tests that every endpoint is using the pagination correctly. + + Raises: + AssertionError: An endpoint did not have one the following keyword + in the JSOn response: 'count', 'next', 'previous', 'results' + or more that 100 results were returned. """ self.client.force_authenticate(self.superuser) for url in self.endpoints: diff --git a/api/urls.py b/api/urls.py index edf399c8..9353975f 100644 --- a/api/urls.py +++ b/api/urls.py @@ -2,7 +2,7 @@ # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # -# Copyright © 2018 Mael Kervella +# 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 @@ -17,27 +17,30 @@ # 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 +"""Defines the URLs of the API + +A custom router is used to register all the routes. That allows to register +all the URL patterns from the viewsets as a standard router but, the views +can also be register. That way a complete API root page presenting all URLs +can be generated automatically. """ -from __future__ import unicode_literals - from django.conf.urls import url, include -from .routers import AllViewsRouter from . import views +from .routers import AllViewsRouter + router = AllViewsRouter() -# COTISATIONS APP +# COTISATIONS router.register_viewset(r'cotisations/factures', views.FactureViewSet) router.register_viewset(r'cotisations/ventes', views.VenteViewSet) router.register_viewset(r'cotisations/articles', views.ArticleViewSet) router.register_viewset(r'cotisations/banques', views.BanqueViewSet) router.register_viewset(r'cotisations/paiements', views.PaiementViewSet) router.register_viewset(r'cotisations/cotisations', views.CotisationViewSet) -# MACHINES APP +# MACHINES router.register_viewset(r'machines/machines', views.MachineViewSet) router.register_viewset(r'machines/machinetypes', views.MachineTypeViewSet) router.register_viewset(r'machines/iptypes', views.IpTypeViewSet) @@ -57,7 +60,7 @@ router.register_viewset(r'machines/services', views.ServiceViewSet) router.register_viewset(r'machines/servicelinks', views.ServiceLinkViewSet, base_name='servicelink') router.register_viewset(r'machines/ouvertureportlists', views.OuverturePortListViewSet) router.register_viewset(r'machines/ouvertureports', views.OuverturePortViewSet) -# PREFERENCES APP +# PREFERENCES router.register_viewset(r'preferences/service', views.ServiceViewSet), router.register_view(r'preferences/optionaluser', views.OptionalUserView), router.register_view(r'preferences/optionalmachine', views.OptionalMachineView), @@ -66,7 +69,7 @@ router.register_view(r'preferences/generaloption', views.GeneralOptionView), router.register_view(r'preferences/assooption', views.AssoOptionView), router.register_view(r'preferences/homeoption', views.HomeOptionView), router.register_view(r'preferences/mailmessageoption', views.MailMessageOptionView), -# TOPOLOGIE APP +# TOPOLOGIE router.register_viewset(r'topologie/stack', views.StackViewSet) router.register_viewset(r'topologie/acesspoint', views.AccessPointViewSet) router.register_viewset(r'topologie/switch', views.SwitchViewSet) @@ -76,7 +79,7 @@ router.register_viewset(r'topologie/switchbay', views.SwitchBayViewSet) router.register_viewset(r'topologie/building', views.BuildingViewSet) router.register_viewset(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport') router.register_viewset(r'topologie/room', views.RoomViewSet) -# USERS APP +# USERS router.register_viewset(r'users/users', views.UserViewSet) router.register_viewset(r'users/clubs', views.ClubViewSet) router.register_viewset(r'users/adherents', views.AdherentViewSet) @@ -86,7 +89,7 @@ router.register_viewset(r'users/listrights', views.ListRightViewSet) router.register_viewset(r'users/shells', views.ShellViewSet, base_name='shell') router.register_viewset(r'users/bans', views.BanViewSet) router.register_viewset(r'users/whitelists', views.WhitelistViewSet) -# SERVICES REGEN +# SERVICE REGEN router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name='serviceregen') # DHCP router.register_view(r'dhcp/hostmacip', views.HostMacIpView), @@ -95,7 +98,7 @@ router.register_view(r'dns/zones', views.DNSZonesView), # MAILING router.register_view(r'mailing/standard', views.StandardMailingView), router.register_view(r'mailing/club', views.ClubMailingView), -# TOKEN-AUTH +# TOKEN AUTHENTICATION router.register_view(r'token-auth', views.ObtainExpiringAuthToken) diff --git a/api/views.py b/api/views.py index 7d4aaffc..9ee6ea75 100644 --- a/api/views.py +++ b/api/views.py @@ -18,16 +18,16 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""api.views +"""Defines the views of the API -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. +All views inherits the `rest_framework.views.APIview` to respect the +REST API requirements such as dealing with HTTP status code, format of +the response (JSON or other), the CSRF exempting, ... """ import datetime from django.conf import settings - from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.authtoken.models import Token from rest_framework.response import Response @@ -38,7 +38,6 @@ import machines.models as machines import preferences.models as preferences import topologie.models as topologie import users.models as users - from re2o.utils import all_active_interfaces, all_has_access from . import serializers @@ -46,142 +45,195 @@ from .pagination import PageSizedPagination from .permissions import ACLPermission -# COTISATIONS APP +# COTISATIONS class FactureViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `cotisations.models.Facture` objects. + """ queryset = cotisations.Facture.objects.all() serializer_class = serializers.FactureSerializer class VenteViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `cotisations.models.Vente` objects. + """ queryset = cotisations.Vente.objects.all() serializer_class = serializers.VenteSerializer class ArticleViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `cotisations.models.Article` objects. + """ queryset = cotisations.Article.objects.all() serializer_class = serializers.ArticleSerializer class BanqueViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `cotisations.models.Banque` objects. + """ queryset = cotisations.Banque.objects.all() serializer_class = serializers.BanqueSerializer class PaiementViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `cotisations.models.Paiement` objects. + """ queryset = cotisations.Paiement.objects.all() serializer_class = serializers.PaiementSerializer class CotisationViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `cotisations.models.Cotisation` objects. + """ queryset = cotisations.Cotisation.objects.all() serializer_class = serializers.CotisationSerializer -# MACHINES APP +# MACHINES class MachineViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Machine` objects. + """ queryset = machines.Machine.objects.all() serializer_class = serializers.MachineSerializer class MachineTypeViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.MachineType` objects. + """ queryset = machines.MachineType.objects.all() serializer_class = serializers.MachineTypeSerializer class IpTypeViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.IpType` objects. + """ queryset = machines.IpType.objects.all() serializer_class = serializers.IpTypeSerializer class VlanViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Vlan` objects. + """ queryset = machines.Vlan.objects.all() serializer_class = serializers.VlanSerializer class NasViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Nas` objects. + """ queryset = machines.Nas.objects.all() serializer_class = serializers.NasSerializer class SOAViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.SOA` objects. + """ queryset = machines.SOA.objects.all() serializer_class = serializers.SOASerializer class ExtensionViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Extension` objects. + """ queryset = machines.Extension.objects.all() serializer_class = serializers.ExtensionSerializer class MxViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Mx` objects. + """ queryset = machines.Mx.objects.all() serializer_class = serializers.MxSerializer class NsViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Ns` objects. + """ queryset = machines.Ns.objects.all() serializer_class = serializers.NsSerializer class TxtViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Txt` objects. + """ queryset = machines.Txt.objects.all() serializer_class = serializers.TxtSerializer class SrvViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Srv` objects. + """ queryset = machines.Srv.objects.all() serializer_class = serializers.SrvSerializer class InterfaceViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Interface` objects. + """ queryset = machines.Interface.objects.all() serializer_class = serializers.InterfaceSerializer class Ipv6ListViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Ipv6List` objects. + """ queryset = machines.Ipv6List.objects.all() serializer_class = serializers.Ipv6ListSerializer class DomainViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Domain` objects. + """ queryset = machines.Domain.objects.all() serializer_class = serializers.DomainSerializer class IpListViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.IpList` objects. + """ queryset = machines.IpList.objects.all() serializer_class = serializers.IpListSerializer class ServiceViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Service` objects. + """ queryset = machines.Service.objects.all() serializer_class = serializers.ServiceSerializer class ServiceLinkViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Service_link` objects. + """ queryset = machines.Service_link.objects.all() serializer_class = serializers.ServiceLinkSerializer class OuverturePortListViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.OuverturePortList` + objects. + """ queryset = machines.OuverturePortList.objects.all() serializer_class = serializers.OuverturePortListSerializer class OuverturePortViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.OuverturePort` objects. + """ queryset = machines.OuverturePort.objects.all() serializer_class = serializers.OuverturePortSerializer -# PREFERENCES APP +# PREFERENCES # Those views differ a bit because there is only one object # to display, so we don't bother with the listing part class OptionalUserView(generics.RetrieveAPIView): + """Exposes details of `preferences.models.` settings. + """ permission_classes = (ACLPermission, ) perms_map = {'GET' : [preferences.OptionalUser.can_view_all]} serializer_class = serializers.OptionalUserSerializer @@ -191,6 +243,8 @@ class OptionalUserView(generics.RetrieveAPIView): class OptionalMachineView(generics.RetrieveAPIView): + """Exposes details of `preferences.models.OptionalMachine` settings. + """ permission_classes = (ACLPermission, ) perms_map = {'GET' : [preferences.OptionalMachine.can_view_all]} serializer_class = serializers.OptionalMachineSerializer @@ -200,6 +254,8 @@ class OptionalMachineView(generics.RetrieveAPIView): class OptionalTopologieView(generics.RetrieveAPIView): + """Exposes details of `preferences.models.OptionalTopologie` settings. + """ permission_classes = (ACLPermission, ) perms_map = {'GET' : [preferences.OptionalTopologie.can_view_all]} serializer_class = serializers.OptionalTopologieSerializer @@ -209,6 +265,8 @@ class OptionalTopologieView(generics.RetrieveAPIView): class GeneralOptionView(generics.RetrieveAPIView): + """Exposes details of `preferences.models.GeneralOption` settings. + """ permission_classes = (ACLPermission, ) perms_map = {'GET' : [preferences.GeneralOption.can_view_all]} serializer_class = serializers.GeneralOptionSerializer @@ -218,11 +276,15 @@ class GeneralOptionView(generics.RetrieveAPIView): class ServiceViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `preferences.models.Service` objects. + """ queryset = preferences.Service.objects.all() serializer_class = serializers.ServiceSerializer class AssoOptionView(generics.RetrieveAPIView): + """Exposes details of `preferences.models.AssoOption` settings. + """ permission_classes = (ACLPermission, ) perms_map = {'GET' : [preferences.AssoOption.can_view_all]} serializer_class = serializers.AssoOptionSerializer @@ -232,6 +294,8 @@ class AssoOptionView(generics.RetrieveAPIView): class HomeOptionView(generics.RetrieveAPIView): + """Exposes details of `preferences.models.HomeOption` settings. + """ permission_classes = (ACLPermission, ) perms_map = {'GET' : [preferences.HomeOption.can_view_all]} serializer_class = serializers.HomeOptionSerializer @@ -241,6 +305,8 @@ class HomeOptionView(generics.RetrieveAPIView): class MailMessageOptionView(generics.RetrieveAPIView): + """Exposes details of `preferences.models.MailMessageOption` settings. + """ permission_classes = (ACLPermission, ) perms_map = {'GET' : [preferences.MailMessageOption.can_view_all]} serializer_class = serializers.MailMessageOptionSerializer @@ -249,106 +315,145 @@ class MailMessageOptionView(generics.RetrieveAPIView): return preferences.MailMessageOption.objects.first() -# TOPOLOGIE APP +# TOPOLOGIE class StackViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.Stack` objects. + """ queryset = topologie.Stack.objects.all() serializer_class = serializers.StackSerializer class AccessPointViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.AccessPoint` objects. + """ queryset = topologie.AccessPoint.objects.all() serializer_class = serializers.AccessPointSerializer class SwitchViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.Switch` objects. + """ queryset = topologie.Switch.objects.all() serializer_class = serializers.SwitchSerializer class ModelSwitchViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.ModelSwitch` objects. + """ queryset = topologie.ModelSwitch.objects.all() serializer_class = serializers.ModelSwitchSerializer class ConstructorSwitchViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.ConstructorSwitch` + objects. + """ queryset = topologie.ConstructorSwitch.objects.all() serializer_class = serializers.ConstructorSwitchSerializer class SwitchBayViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.SwitchBay` objects. + """ queryset = topologie.SwitchBay.objects.all() serializer_class = serializers.SwitchBaySerializer class BuildingViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.Building` objects. + """ queryset = topologie.Building.objects.all() serializer_class = serializers.BuildingSerializer class SwitchPortViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.Port` objects. + """ queryset = topologie.Port.objects.all() serializer_class = serializers.SwitchPortSerializer class RoomViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.Room` objects. + """ queryset = topologie.Room.objects.all() serializer_class = serializers.RoomSerializer -# USER APP +# USER class UserViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `users.models.Users` objects. + """ queryset = users.User.objects.all() serializer_class = serializers.UserSerializer class ClubViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `users.models.Club` objects. + """ queryset = users.Club.objects.all() serializer_class = serializers.ClubSerializer class AdherentViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `users.models.Adherent` objects. + """ queryset = users.Adherent.objects.all() serializer_class = serializers.AdherentSerializer class ServiceUserViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `users.models.ServiceUser` objects. + """ queryset = users.ServiceUser.objects.all() serializer_class = serializers.ServiceUserSerializer class SchoolViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `users.models.School` objects. + """ queryset = users.School.objects.all() serializer_class = serializers.SchoolSerializer class ListRightViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `users.models.ListRight` objects. + """ queryset = users.ListRight.objects.all() serializer_class = serializers.ListRightSerializer class ShellViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `users.models.ListShell` objects. + """ queryset = users.ListShell.objects.all() serializer_class = serializers.ShellSerializer class BanViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `users.models.Ban` objects. + """ queryset = users.Ban.objects.all() serializer_class = serializers.BanSerializer class WhitelistViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `users.models.Whitelist` objects. + """ queryset = users.Whitelist.objects.all() serializer_class = serializers.WhitelistSerializer -# Services views +# SERVICE REGEN class ServiceRegenViewSet(viewsets.ModelViewSet): + """Exposes list and details of the services to regen + """ serializer_class = serializers.ServiceRegenSerializer def get_queryset(self): @@ -363,24 +468,33 @@ class ServiceRegenViewSet(viewsets.ModelViewSet): return queryset -# DHCP views +# DHCP class HostMacIpView(generics.ListAPIView): + """Exposes the associations between hostname, mac address and IPv4 in + order to build the DHCP lease files. + """ queryset = all_active_interfaces() serializer_class = serializers.HostMacIpSerializer -# DNS views +# DNS class DNSZonesView(generics.ListAPIView): + """Exposes the detailed information about each extension (hostnames, + IPs, DNS records, etc.) in order to build the DNS zone files. + """ queryset = machines.Extension.objects.all() serializer_class = serializers.DNSZonesSerializer -# Mailing views +# MAILING class StandardMailingView(views.APIView): + """Exposes list and details of standard mailings (name and members) in + order to building the corresponding mailing lists. + """ pagination_class = PageSizedPagination permission_classes = (ACLPermission, ) perms_map = {'GET' : [users.User.can_view_all]} @@ -394,13 +508,23 @@ class StandardMailingView(views.APIView): class ClubMailingView(generics.ListAPIView): + """Exposes list and details of club mailings (name, members and admins) in + order to build the corresponding mailing lists. + """ queryset = users.Club.objects.all() serializer_class = serializers.MailingSerializer -# Subclass the standard rest_framework.auth_token.views.ObtainAuthToken -# in order to renew the lease of the token and add expiration time +# TOKEN AUTHENTICATION + + class ObtainExpiringAuthToken(ObtainAuthToken): + """Exposes a view to obtain a authentication token. + + This view as the same behavior as the + `rest_framework.auth_token.views.ObtainAuthToken` view except that the + expiration time is send along with the token as an addtional information. + """ def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True)