diff --git a/CHANGELOG.md b/CHANGELOG.md index b5291b56..41982264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ mkdir -p media/images ## MR 163: Fix install re2o Refactored install_re2o.sh script. -* There are more tools available with it but some fucntion have changed, report to [the dedicated wiki page](for more informations) or run: +* There are more tools available with it but some function have changed, report to [the dedicated wiki page](https://gitlab.federez.net/federez/re2o/wikis/User%20Documentation/Setup%20script)for more informations or run: ``` install_re2o.sh help ``` @@ -53,3 +53,22 @@ Add the logo and fix somme issues on the navbar and home page. Only collecting t python3 manage.py collectstatic ``` + +## MR 172: Refactor API + +Creates a new (nearly) REST API to expose all models of Re2o. See [the dedicated wiki page](https://gitlab.federez.net/federez/re2o/wikis/API/Raw-Usage) for more details on how to use it. +* For testing purpose, add `volatildap` package: +``` +pip3 install volatildap +``` +* Activate HTTP Authorization passthrough in by adding the following in `/etc/apache2/site-available/re2o.conf` (example in `install_utils/apache2/re2o.conf`): +``` + WSGIPassAuthorization On +``` +* Activate the API if you want to use it by adding `'api'` to the optional apps in `re2o/settings_local.py`: +``` +OPTIONAL_APPS = ( + ... + 'api', + ... +) \ No newline at end of file diff --git a/api/acl.py b/api/acl.py new file mode 100644 index 00000000..8c39aed0 --- /dev/null +++ b/api/acl.py @@ -0,0 +1,73 @@ +# 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 ACL for the whole API. + +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 _ + + +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): + """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). + """ + kwargs = { + 'app_label': settings.API_CONTENT_TYPE_APP_LABEL, + 'codename': settings.API_PERMISSION_CODENAME + } + can = user.has_perm('%(app_label)s.%(codename)s' % kwargs) + return can, None if can else _("You cannot see this application.") diff --git a/api/authentication.py b/api/authentication.py new file mode 100644 index 00000000..469c51f1 --- /dev/null +++ b/api/authentication.py @@ -0,0 +1,48 @@ +# 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): + """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 + ) + utc_now = datetime.datetime.now(datetime.timezone.utc) + if token.created < utc_now - token_duration: + raise exceptions.AuthenticationFailed(_('Token has expired')) + + return (token.user, token) diff --git a/api/pagination.py b/api/pagination.py new file mode 100644 index 00000000..20dcad6e --- /dev/null +++ b/api/pagination.py @@ -0,0 +1,62 @@ +# 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): + """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: + return self.max_page_size + except KeyError: + pass + + return super(PageSizedPagination, self).get_page_size(request) diff --git a/api/permissions.py b/api/permissions.py new file mode 100644 index 00000000..53f06620 --- /dev/null +++ b/api/permissions.py @@ -0,0 +1,284 @@ +# 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 ' + '`.{}` or have a `.get_{}()` method.' + ).format(self.__class__.__name__, param_name, param_name) + + if hasattr(view, 'get_'+param_name): + param = getattr(view, 'get_'+param_name)() + assert param is not None, ( + '{}.get_{}() returned None' + ).format(view.__class__.__name__, param_name) + return param + return getattr(view, param_name) + + +class ACLPermission(permissions.BasePermission): + """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): + """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') + + if method not in perms_map: + raise exceptions.MethodNotAllowed(method) + + 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): + return True + + if not request.user or not request.user.is_authenticated: + return False + + perms = self.get_required_permissions(request.method, view) + + return all(perm(request.user)[0] for perm in perms) + + +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. + """ + + perms_map = { + 'GET': [can_see_api, lambda model: model.can_view_all], + 'OPTIONS': [can_see_api, lambda model: model.can_view_all], + 'HEAD': [can_see_api, lambda model: model.can_view_all], + 'POST': [can_see_api, lambda model: model.can_create], + 'PUT': [], # No restrictions, apply to objects + 'PATCH': [], # No restrictions, apply to objects + 'DELETE': [], # No restrictions, apply to objects + } + perms_obj_map = { + 'GET': [can_see_api, lambda obj: obj.can_view], + 'OPTIONS': [can_see_api, lambda obj: obj.can_view], + 'HEAD': [can_see_api, lambda obj: obj.can_view], + 'POST': [], # No restrictions, apply to models + 'PUT': [can_see_api, lambda obj: obj.can_edit], + 'PATCH': [can_see_api, lambda obj: obj.can_edit], + 'DELETE': [can_see_api, lambda obj: obj.can_delete], + } + + def get_required_permissions(self, method, model): + """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) + + return [perm(model) for perm in self.perms_map[method]] + + def get_required_object_permissions(self, method, obj): + """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) + + return [perm(obj) for perm in self.perms_obj_map[method]] + + def _queryset(self, view): + 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): + return True + + if not request.user or not request.user.is_authenticated: + return False + + queryset = self._queryset(view) + perms = self.get_required_permissions(request.method, queryset.model) + + 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 + user = request.user + + perms = self.get_required_object_permissions(request.method, obj) + + if not all(perm(request.user)[0] for perm in perms): + # If the user does not have permissions we need to determine if + # they have read permissions to see 403, or not, and simply see + # a 404 response. + + if request.method in SAFE_METHODS: + # Read permissions already checked and failed, no need + # to make another lookup. + raise Http404 + + read_perms = self.get_required_object_permissions('GET', obj) + if not read_perms(request.user)[0]: + raise Http404 + + # Has read permissions. + return False + + return True + diff --git a/api/routers.py b/api/routers.py new file mode 100644 index 00000000..fcfb5077 --- /dev/null +++ b/api/routers.py @@ -0,0 +1,157 @@ +# 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. + +"""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 +from rest_framework.routers import DefaultRouter +from rest_framework.response import Response +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. + + 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. + + 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): + """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 + for prefix, viewset, basename in self.registry: + api_root_dict[prefix] = list_name.format(basename=basename) + for pattern, view, name in self.view_registry: + api_root_dict[pattern] = name + + view_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) + schema_media_types = [] + + if schema_urls and self.schema_title: + view_renderers += list(self.schema_renderers) + schema_generator = SchemaGenerator( + title=self.schema_title, + patterns=schema_urls + ) + schema_media_types = [ + renderer.media_type + for renderer in self.schema_renderers + ] + + class APIRoot(views.APIView): + _ignore_model_permissions = True + renderer_classes = view_renderers + + def get(self, request, *args, **kwargs): + if request.accepted_renderer.media_type in schema_media_types: + # Return a schema response. + schema = schema_generator.get_schema(request) + if schema is None: + raise exceptions.PermissionDenied() + return Response(schema) + + # Return a plain {"name": "hyperlink"} response. + ret = OrderedDict() + namespace = request.resolver_match.namespace + for key, url_name in api_root_dict.items(): + if namespace: + url_name = namespace + ':' + url_name + try: + ret[key] = reverse( + url_name, + args=args, + kwargs=kwargs, + request=request, + format=kwargs.get('format', None) + ) + except NoReverseMatch: + # Don't bail out if eg. no list routes exist, only detail routes. + continue + + return Response(ret) + + 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: + urls.append(url(pattern, view.as_view(), name=name)) + + return urls diff --git a/api/serializers.py b/api/serializers.py index f2604068..48988365 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,429 +18,731 @@ # 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 -from users.models import Club, Adherent -from machines.models import ( - Interface, - IpType, - Extension, - IpList, - Domain, - Txt, - Mx, - Srv, - Service_link, - Ns, - OuverturePort, - Ipv6List -) + +import cotisations.models as cotisations +import machines.models as machines +import preferences.models as preferences +import topologie.models as topologie +import users.models as users -class ServiceLinkSerializer(serializers.ModelSerializer): - """ Serializer for the ServiceLink objects """ +# The namespace used for the API. It must match the namespace used in the +# urlpatterns to include the API URLs. +API_NAMESPACE = 'api' - name = serializers.CharField(source='service.service_type') + +class NamespacedHRField(serializers.HyperlinkedRelatedField): + """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) + super(NamespacedHRField, self).__init__(view_name=view_name, **kwargs) + + +class NamespacedHIField(serializers.HyperlinkedIdentityField): + """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) + super(NamespacedHIField, self).__init__(view_name=view_name, **kwargs) + + +class NamespacedHMSerializer(serializers.HyperlinkedModelSerializer): + """A `rest_framework.serializers.HyperlinkedModelSerializer` subclass to + automatically prefix view names with the API namespace. + """ + serializer_related_field = NamespacedHRField + serializer_url_field = NamespacedHIField + + +# COTISATIONS + + +class FactureSerializer(NamespacedHMSerializer): + """Serialize `cotisations.models.Facture` objects. + """ + class Meta: + model = cotisations.Facture + fields = ('user', 'paiement', 'banque', 'cheque', 'date', 'valid', + 'control', 'prix_total', 'name', 'api_url') + + +class VenteSerializer(NamespacedHMSerializer): + """Serialize `cotisations.models.Vente` objects. + """ + class Meta: + model = cotisations.Vente + fields = ('facture', 'number', 'name', 'prix', 'duration', + 'type_cotisation', 'prix_total', 'api_url') + + +class ArticleSerializer(NamespacedHMSerializer): + """Serialize `cotisations.models.Article` objects. + """ + class Meta: + model = cotisations.Article + fields = ('name', 'prix', 'duration', 'type_user', + 'type_cotisation', 'api_url') + + +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 + + +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', + 'domaine_ip_stop', 'prefix_v6', 'vlan', 'ouverture_ports', + 'api_url') + + +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', + 'autocapture_mac', 'api_url') + + +class SOASerializer(NamespacedHMSerializer): + """Serialize `machines.models.SOA` objects. + """ + class Meta: + model = machines.SOA + fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl', + 'api_url') + + +class ExtensionSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Extension` objects. + """ + class Meta: + model = machines.Extension + fields = ('name', 'need_infra', 'origin', 'origin_v6', 'soa', + 'api_url') + + +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', + 'weight', 'port', 'target', 'api_url') + + +class InterfaceSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Interface` objects. + """ + mac_address = serializers.CharField() + active = serializers.BooleanField(source='is_active') class Meta: - model = Service_link - fields = ('name',) + model = machines.Interface + fields = ('ipv4', 'mac_address', 'machine', 'type', 'details', + 'port_lists', 'active', 'api_url') -class MailingSerializer(serializers.ModelSerializer): - """ Serializer to build Mailing objects """ - - name = serializers.CharField(source='pseudo') - +class Ipv6ListSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Ipv6List` objects. + """ class Meta: - model = Club - fields = ('name',) + model = machines.Ipv6List + fields = ('ipv6', 'interface', 'slaac_ip', 'api_url') -class MailingMemberSerializer(serializers.ModelSerializer): - """ Serializer fot the Adherent objects (who belong to a - Mailing) """ - +class DomainSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Domain` objects. + """ class Meta: - model = Adherent - fields = ('email',) + model = machines.Domain + fields = ('interface_parent', 'name', 'extension', 'cname', + 'api_url') -class IpTypeField(serializers.RelatedField): - """ Serializer for an IpType object field """ - - def to_representation(self, value): - return value.type - - def to_internal_value(self, data): - pass - - -class IpListSerializer(serializers.ModelSerializer): - """ Serializer for an Ipv4List obejct using the IpType serialization """ - - ip_type = IpTypeField(read_only=True) - +class IpListSerializer(NamespacedHMSerializer): + """Serialize `machines.models.IpList` objects. + """ class Meta: - model = IpList - fields = ('ipv4', 'ip_type') + model = machines.IpList + fields = ('ipv4', 'ip_type', 'need_infra', 'api_url') -class Ipv6ListSerializer(serializers.ModelSerializer): - """ Serializer for an Ipv6List object """ - +class ServiceSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Service` objects. + """ class Meta: - model = Ipv6List - fields = ('ipv6', 'slaac_ip') + model = machines.Service + fields = ('service_type', 'min_time_regen', 'regular_time_regen', + 'servers', 'api_url') -class InterfaceSerializer(serializers.ModelSerializer): - """ Serializer for an Interface object. Use SerializerMethodField - to get ForeignKey values """ - - ipv4 = IpListSerializer(read_only=True) - # TODO : use serializer.RelatedField to avoid duplicate code - mac_address = serializers.SerializerMethodField('get_macaddress') - domain = serializers.SerializerMethodField('get_dns') - extension = serializers.SerializerMethodField('get_interface_extension') - +class ServiceLinkSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Service_link` objects. + """ class Meta: - model = Interface - fields = ('ipv4', 'mac_address', 'domain', 'extension') - - @staticmethod - def get_dns(obj): - """ The name of the associated DNS object """ - return obj.domain.name - - @staticmethod - def get_interface_extension(obj): - """ The name of the associated Interface object """ - return obj.domain.extension.name - - @staticmethod - def get_macaddress(obj): - """ The string representation of the associated MAC address """ - return str(obj.mac_address) - - -class FullInterfaceSerializer(serializers.ModelSerializer): - """ Serializer for an Interface obejct. Use SerializerMethodField - to get ForeignKey values """ - - ipv4 = IpListSerializer(read_only=True) - ipv6 = Ipv6ListSerializer(read_only=True, many=True) - # TODO : use serializer.RelatedField to avoid duplicate code - 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') - - @staticmethod - def get_dns(obj): - """ The name of the associated DNS object """ - return obj.domain.name - - @staticmethod - def get_interface_extension(obj): - """ The name of the associated Extension object """ - return obj.domain.extension.name - - @staticmethod - def get_macaddress(obj): - """ The string representation of the associated MAC address """ - return str(obj.mac_address) - - -class ExtensionNameField(serializers.RelatedField): - """ Serializer for Extension object field """ - - def to_representation(self, value): - return value.name - - def to_internal_value(self, data): - pass - - -class TypeSerializer(serializers.ModelSerializer): - """ Serializer for an IpType object. Use SerializerMethodField to - get ForeignKey values """ - - 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',) - - @staticmethod - def get_port_policy(obj, protocole, io): - """ Generic utility function to get the policy for a given - port, protocole and IN or OUT """ - 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') - - @staticmethod - def get_origin_ip(obj): - """ The IP of the associated origin for the zone """ - return obj.origin.ipv4 - - @staticmethod - def get_zone_name(obj): - """ The name of the associated zone """ - return str(obj.dns_entry) - - @staticmethod - def get_soa_data(obj): - """ The representation of the associated SOA """ - 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') - - @staticmethod - def get_entry_name(obj): - """ The name of the DNS MX entry """ - return str(obj.name) - - @staticmethod - def get_zone_name(obj): - """ The name of the associated zone of the MX record """ - return obj.zone.name - - @staticmethod - def get_mx_name(obj): - """ The string representation of the entry to add to the DNS """ - 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') - - @staticmethod - def get_zone_name(obj): - """ The name of the associated zone """ - return str(obj.zone.name) - - @staticmethod - def get_txt_name(obj): - """ The string representation of the entry to add to the DNS """ - 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' - ) - - @staticmethod - def get_extension_name(obj): - """ The name of the associated extension """ - return str(obj.extension.name) - - @staticmethod - def get_srv_name(obj): - """ The string representation of the entry to add to the DNS """ - 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') - - @staticmethod - def get_zone_name(obj): - """ The name of the associated zone """ - return obj.zone.name - - @staticmethod - def get_domain_name(obj): - """ The name of the associated NS target """ - return str(obj.ns) - - @staticmethod - def get_text_name(obj): - """ The string representation of the entry to add to the DNS """ - 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') - - @staticmethod - def get_zone_name(obj): - """ The name of the associated zone """ - return obj.extension.name - - @staticmethod - def get_alias_name(obj): - """ The name of the associated alias """ - return str(obj.cname) - - @staticmethod - def get_cname_name(obj): - """ The name of the associated CNAME target """ - 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') - - @staticmethod - def get_server_name(obj): - """ The name of the associated server """ - return str(obj.server.domain.name) - - @staticmethod - def get_service_name(obj): - """ The name of the service name """ - return str(obj.service) - - @staticmethod - def get_regen_status(obj): - """ The string representation of the regen status """ - return obj.need_regen() - - -class OuverturePortsSerializer(serializers.Serializer): - """Serialisation de l'ouverture des ports""" - ipv4 = serializers.SerializerMethodField() - ipv6 = serializers.SerializerMethodField() - - def create(self, validated_data): - """ Creates a new object based on the un-serialized data. - Used to implement an abstract inherited method """ - pass - - def update(self, instance, validated_data): - """ Updates an object based on the un-serialized data. - Used to implement an abstract inherited method """ - pass - - @staticmethod - def get_ipv4(): - """ The representation of the policy for the IPv4 addresses """ - 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 + model = machines.Service_link + fields = ('service', 'server', 'last_regen', 'asked_regen', + 'need_regen', 'api_url') + extra_kwargs = { + 'api_url': {'view_name': 'servicelink-detail'} } - @staticmethod - def get_ipv6(): - """ The representation of the policy for the IPv6 addresses """ - 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 + +class OuverturePortListSerializer(NamespacedHMSerializer): + """Serialize `machines.models.OuverturePortList` objects. + """ + tcp_ports_in = NamespacedHRField(view_name='ouvertureport-detail', many=True, read_only=True) + udp_ports_in = NamespacedHRField(view_name='ouvertureport-detail', many=True, read_only=True) + tcp_ports_out = NamespacedHRField(view_name='ouvertureport-detail', many=True, read_only=True) + udp_ports_out = NamespacedHRField(view_name='ouvertureport-detail', many=True, read_only=True) + + class Meta: + model = machines.OuverturePortList + fields = ('name', 'tcp_ports_in', 'udp_ports_in', 'tcp_ports_out', + 'udp_ports_out', 'api_url') + + +class OuverturePortSerializer(NamespacedHMSerializer): + """Serialize `machines.models.OuverturePort` objects. + """ + class Meta: + model = machines.OuverturePort + fields = ('begin', 'end', 'port_list', 'protocole', 'io', 'api_url') + + +# PREFERENCES + + +class OptionalUserSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.OptionalUser` objects. + """ + tel_mandatory = serializers.BooleanField(source='is_tel_mandatory') + + class Meta: + model = preferences.OptionalUser + fields = ('tel_mandatory', 'user_solde', 'solde_negatif', 'max_solde', + 'min_online_payment', 'gpg_fingerprint', + 'all_can_create_club', 'self_adhesion', 'shell_default') + + +class OptionalMachineSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.OptionalMachine` objects. + """ + class Meta: + model = preferences.OptionalMachine + fields = ('password_machine', 'max_lambdauser_interfaces', + 'max_lambdauser_aliases', 'ipv6_mode', 'create_machine', + 'ipv6') + + +class OptionalTopologieSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.OptionalTopologie` objects. + """ + class Meta: + model = preferences.OptionalTopologie + fields = ('radius_general_policy', 'vlan_decision_ok', + 'vlan_decision_nok') + + +class GeneralOptionSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.GeneralOption` objects. + """ + class Meta: + model = preferences.GeneralOption + fields = ('general_message', 'search_display_page', + 'pagination_number', 'pagination_large_number', + 'req_expire_hrs', 'site_name', 'email_from', 'GTU_sum_up', + 'GTU') + + +class HomeServiceSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.Service` objects. + """ + class Meta: + model = preferences.Service + fields = ('name', 'url', 'description', 'image', 'api_url') + extra_kwargs = { + 'api_url': {'view_name': 'homeservice-detail'} } + + +class AssoOptionSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.AssoOption` objects. + """ + class Meta: + model = preferences.AssoOption + fields = ('name', 'siret', 'adresse1', 'adresse2', 'contact', + 'telephone', 'pseudo', 'utilisateur_asso', 'payment', + 'payment_id', 'payment_pass', 'description') + + +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 + + +class StackSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Stack` objects + """ + class Meta: + model = topologie.Stack + fields = ('name', 'stack_id', 'details', 'member_id_min', + 'member_id_max', 'api_url') + + +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 + fields = ('user', 'name', 'active', 'port_amount', 'stack', + 'stack_member_id', 'model', 'switchbay', 'api_url') + + +class ServerSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Server` objects + """ + class Meta: + model = topologie.Server + fields = ('user', 'name', 'active', 'api_url') + + +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', + 'radius', 'vlan_force', 'details', 'api_url') + extra_kwargs = { + 'related': {'view_name': 'switchport-detail'}, + 'api_url': {'view_name': 'switchport-detail'} + } + + +class RoomSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Room` objects + """ + class Meta: + model = topologie.Room + fields = ('name', 'details', 'api_url') + + +# USERS + + +class UserSerializer(NamespacedHMSerializer): + """Serialize `users.models.User` objects. + """ + access = serializers.BooleanField(source='has_access') + uid = serializers.IntegerField(source='uid_number') + + class Meta: + model = users.User + fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment', + 'state', 'registered', 'telephone', 'solde', 'access', + 'end_access', 'uid', 'class_name', 'api_url') + extra_kwargs = { + 'shell': {'view_name': 'shell-detail'} + } + + +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') + + class Meta: + model = users.Club + fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment', + 'state', 'registered', 'telephone', 'solde', 'room', + 'access', 'end_access', 'administrators', 'members', + 'mailing', 'uid', 'api_url') + extra_kwargs = { + 'shell': {'view_name': 'shell-detail'} + } + + +class AdherentSerializer(NamespacedHMSerializer): + """Serialize `users.models.Adherent` objects. + """ + access = serializers.BooleanField(source='has_access') + uid = serializers.IntegerField(source='uid_number') + + class Meta: + model = users.Adherent + fields = ('name', 'surname', 'pseudo', 'email', 'school', 'shell', + 'comment', 'state', 'registered', 'telephone', 'room', + 'solde', 'access', 'end_access', 'uid', 'api_url') + extra_kwargs = { + 'shell': {'view_name': 'shell-detail'} + } + + +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') + extra_kwargs = { + 'api_url': {'view_name': 'shell-detail'} + } + + +class BanSerializer(NamespacedHMSerializer): + """Serialize `users.models.Ban` objects. + """ + active = serializers.BooleanField(source='is_active') + + class Meta: + model = users.Ban + fields = ('user', 'raison', 'date_start', 'date_end', 'state', + 'active', 'api_url') + + +class WhitelistSerializer(NamespacedHMSerializer): + """Serialize `users.models.Whitelist` objects. + """ + active = serializers.BooleanField(source='is_active') + + class Meta: + model = users.Whitelist + fields = ('user', 'raison', 'date_start', 'date_end', 'active', 'api_url') + + +# 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() + + class Meta: + model = machines.Service_link + fields = ('hostname', 'service_name', 'need_regen', 'api_url') + extra_kwargs = { + 'api_url': {'view_name': 'serviceregen-detail'} + } + + +# DHCP + + +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) + ipv4 = serializers.CharField(source='ipv4.ipv4', read_only=True) + + class Meta: + model = machines.Interface + fields = ('hostname', 'extension', 'mac_address', 'ipv4') + + +# DNS + + +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 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): + fields = ('target',) + + +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): + fields = ('target', 'priority') + + +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): + fields = ('service', 'protocole', 'ttl', 'priority', 'weight', 'port', 'target') + + +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) + + class Meta: + model = machines.Interface + fields = ('hostname', 'ipv4') + + +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) + + class Meta: + model = machines.Interface + fields = ('hostname', 'ipv6') + + +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) + extension = serializers.CharField(source='extension.name', read_only=True) + + class Meta: + model = machines.Domain + fields = ('alias', 'hostname', 'extension') + + +class DNSZonesSerializer(serializers.ModelSerializer): + """Serialize the data about DNS Zones. + """ + soa = SOARecordSerializer() + ns_records = NSRecordSerializer(many=True, source='ns_set') + originv4 = OriginV4RecordSerializer(source='origin') + originv6 = serializers.CharField(source='origin_v6') + mx_records = MXRecordSerializer(many=True, source='mx_set') + txt_records = TXTRecordSerializer(many=True, source='txt_set') + srv_records = SRVRecordSerializer(many=True, source='srv_set') + a_records = ARecordSerializer(many=True, source='get_associated_a_records') + aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records') + cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records') + + class Meta: + model = machines.Extension + fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6', + 'mx_records', 'txt_records', 'srv_records', 'a_records', + 'aaaa_records', 'cname_records') + + +# 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) + + class Meta(ClubSerializer.Meta): + fields = ('name', 'members', 'admins') diff --git a/api/settings.py b/api/settings.py new file mode 100644 index 00000000..f8171638 --- /dev/null +++ b/api/settings.py @@ -0,0 +1,50 @@ +# 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. + +"""Settings specific to the API. +""" + +# RestFramework config for API +REST_FRAMEWORK = { + 'URL_FIELD_NAME': 'api_url', + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'api.authentication.ExpiringTokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'api.permissions.AutodetectACLPermission', + ), + 'DEFAULT_PAGINATION_CLASS': 'api.pagination.PageSizedPagination', + 'PAGE_SIZE': 100 +} + +# API permission settings +API_CONTENT_TYPE_APP_LABEL = 'api' +API_CONTENT_TYPE_MODEL = 'api' +API_PERMISSION_NAME = 'Can use the API' +API_PERMISSION_CODENAME = 'use_api' + +# Activate token authentication +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 bfcda28f..ef05cec2 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,10 +17,762 @@ # 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 """ -# from django.test import TestCase +import json +import datetime +from rest_framework.test import APITestCase +from requests import codes + +import cotisations.models as cotisations +import machines.models as machines +import preferences.models as preferences +import topologie.models as topologie +import users.models as users + + +class APIEndpointsTestCase(APITestCase): + """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/' + ] + auth_no_perm_endpoints = [] + auth_perm_endpoints = [ + '/api/cotisations/article/', + '/api/cotisations/article/1/', + '/api/cotisations/banque/', + '/api/cotisations/banque/1/', + '/api/cotisations/cotisation/', + '/api/cotisations/cotisation/1/', + '/api/cotisations/facture/', + '/api/cotisations/facture/1/', + '/api/cotisations/paiement/', + '/api/cotisations/paiement/1/', + '/api/cotisations/vente/', + '/api/cotisations/vente/1/', + '/api/machines/domain/', + '/api/machines/domain/1/', + '/api/machines/extension/', + '/api/machines/extension/1/', + '/api/machines/interface/', + '/api/machines/interface/1/', + '/api/machines/iplist/', + '/api/machines/iplist/1/', + '/api/machines/iptype/', + '/api/machines/iptype/1/', + '/api/machines/ipv6list/', + '/api/machines/ipv6list/1/', + '/api/machines/machine/', + '/api/machines/machine/1/', + '/api/machines/machinetype/', + '/api/machines/machinetype/1/', + '/api/machines/mx/', + '/api/machines/mx/1/', + '/api/machines/nas/', + '/api/machines/nas/1/', + '/api/machines/ns/', + '/api/machines/ns/1/', + '/api/machines/ouvertureportlist/', + '/api/machines/ouvertureportlist/1/', + '/api/machines/ouvertureport/', + '/api/machines/ouvertureport/1/', + '/api/machines/servicelink/', + '/api/machines/servicelink/1/', + '/api/machines/service/', + '/api/machines/service/1/', + '/api/machines/soa/', + '/api/machines/soa/1/', + '/api/machines/srv/', + '/api/machines/srv/1/', + '/api/machines/txt/', + '/api/machines/txt/1/', + '/api/machines/vlan/', + '/api/machines/vlan/1/', + '/api/preferences/optionaluser/', + '/api/preferences/optionalmachine/', + '/api/preferences/optionaltopologie/', + '/api/preferences/generaloption/', + '/api/preferences/service/', + '/api/preferences/service/1/', + '/api/preferences/assooption/', + '/api/preferences/homeoption/', + '/api/preferences/mailmessageoption/', + '/api/topologie/acesspoint/', + # 2nd machine to be create (machines_machine_1, topologie_accesspoint_1) + '/api/topologie/acesspoint/2/', + '/api/topologie/building/', + '/api/topologie/building/1/', + '/api/topologie/constructorswitch/', + '/api/topologie/constructorswitch/1/', + '/api/topologie/modelswitch/', + '/api/topologie/modelswitch/1/', + '/api/topologie/room/', + '/api/topologie/room/1/', + '/api/topologie/server/', + # 3rd machine to be create (machines_machine_1, topologie_accesspoint_1, + # topologie_server_1) + '/api/topologie/server/3/', + '/api/topologie/stack/', + '/api/topologie/stack/1/', + '/api/topologie/switch/', + # 4th machine to be create (machines_machine_1, topologie_accesspoint_1, + # topologie_server_1, topologie_switch_1) + '/api/topologie/switch/4/', + '/api/topologie/switchbay/', + '/api/topologie/switchbay/1/', + '/api/topologie/switchport/', + '/api/topologie/switchport/1/', + '/api/topologie/switchport/2/', + '/api/topologie/switchport/3/', + '/api/users/adherent/', + # 3rd user to be create (stduser, superuser, users_adherent_1) + '/api/users/adherent/3/', + '/api/users/ban/', + '/api/users/ban/1/', + '/api/users/club/', + # 4th user to be create (stduser, superuser, users_adherent_1, + # users_club_1) + '/api/users/club/4/', + '/api/users/listright/', +# TODO: Merge !145 +# '/api/users/listright/1/', + '/api/users/school/', + '/api/users/school/1/', + '/api/users/serviceuser/', + '/api/users/serviceuser/1/', + '/api/users/shell/', + '/api/users/shell/1/', + '/api/users/user/', + '/api/users/user/1/', + '/api/users/whitelist/', + '/api/users/whitelist/1/', + '/api/dns/zones/', + '/api/dhcp/hostmacip/', + '/api/mailing/standard', + '/api/mailing/club', + '/api/services/regen/', + ] + not_found_endpoints = [ + '/api/cotisations/article/4242/', + '/api/cotisations/banque/4242/', + '/api/cotisations/cotisation/4242/', + '/api/cotisations/facture/4242/', + '/api/cotisations/paiement/4242/', + '/api/cotisations/vente/4242/', + '/api/machines/domain/4242/', + '/api/machines/extension/4242/', + '/api/machines/interface/4242/', + '/api/machines/iplist/4242/', + '/api/machines/iptype/4242/', + '/api/machines/ipv6list/4242/', + '/api/machines/machine/4242/', + '/api/machines/machinetype/4242/', + '/api/machines/mx/4242/', + '/api/machines/nas/4242/', + '/api/machines/ns/4242/', + '/api/machines/ouvertureportlist/4242/', + '/api/machines/ouvertureport/4242/', + '/api/machines/servicelink/4242/', + '/api/machines/service/4242/', + '/api/machines/soa/4242/', + '/api/machines/srv/4242/', + '/api/machines/txt/4242/', + '/api/machines/vlan/4242/', + '/api/preferences/service/4242/', + '/api/topologie/acesspoint/4242/', + '/api/topologie/building/4242/', + '/api/topologie/constructorswitch/4242/', + '/api/topologie/modelswitch/4242/', + '/api/topologie/room/4242/', + '/api/topologie/server/4242/', + '/api/topologie/stack/4242/', + '/api/topologie/switch/4242/', + '/api/topologie/switchbay/4242/', + '/api/topologie/switchport/4242/', + '/api/users/adherent/4242/', + '/api/users/ban/4242/', + '/api/users/club/4242/', + '/api/users/listright/4242/', + '/api/users/school/4242/', + '/api/users/serviceuser/4242/', + '/api/users/shell/4242/', + '/api/users/user/4242/', + '/api/users/whitelist/4242/', + ] + + stduser = None + superuser = None + + @classmethod + def setUpTestData(cls): + # Be aware that every object created here is never actually committed + # to the database. TestCase uses rollbacks after each test to cancel all + # modifications and recreates the data defined here before each test. + # For more details, see + # https://docs.djangoproject.com/en/1.10/topics/testing/tools/#testcase + + super(APIEndpointsTestCase, cls).setUpClass() + + # A user with no rights + cls.stduser = users.User.objects.create_user( + "apistduser", + "apistduser", + "apistduser@example.net", + "apistduser" + ) + # A user with all the rights + cls.superuser = users.User.objects.create_superuser( + "apisuperuser", + "apisuperuser", + "apisuperuser@example.net", + "apisuperuser" + ) + + # Creates 1 instance for each object so the "details" endpoints + # can be tested too. Objects need to be created in the right order. + # Dependencies (relatedFields, ...) are highlighted by a comment at + # the end of the concerned line (# Dep ). + cls.users_school_1 = users.School.objects.create( + name="users_school_1" + ) + cls.users_school_1.save() + cls.users_listshell_1 = users.ListShell.objects.create( + shell="users_listshell_1" + ) + cls.users_adherent_1 = users.Adherent.objects.create( + password="password", + last_login=datetime.datetime.now(datetime.timezone.utc), + surname="users_adherent_1", + pseudo="usersadherent1", + email="users_adherent_1@example.net", + school=cls.users_school_1, # Dep users.School + shell=cls.users_listshell_1, # Dep users.ListShell + comment="users Adherent 1 comment", + pwd_ntlm="", + state=users.User.STATES[0][0], + registered=datetime.datetime.now(datetime.timezone.utc), + telephone="0123456789", + uid_number=21102, + rezo_rez_uid=21102 + ) + cls.users_user_1 = cls.users_adherent_1 + cls.cotisations_article_1 = cotisations.Article.objects.create( + name="cotisations_article_1", + prix=10, + duration=1, + type_user=cotisations.Article.USER_TYPES[0][0], + type_cotisation=cotisations.Article.COTISATION_TYPE[0][0] + ) + cls.cotisations_banque_1 = cotisations.Banque.objects.create( + name="cotisations_banque_1" + ) + cls.cotisations_paiement_1 = cotisations.Paiement.objects.create( + moyen="cotisations_paiement_1", + type_paiement=cotisations.Paiement.PAYMENT_TYPES[0][0] + ) + cls.cotisations_facture_1 = cotisations.Facture.objects.create( + user=cls.users_user_1, # Dep users.User + paiement=cls.cotisations_paiement_1, # Dep cotisations.Paiement + banque=cls.cotisations_banque_1, # Dep cotisations.Banque + cheque="1234567890", + date=datetime.datetime.now(datetime.timezone.utc), + valid=True, + control=False + ) + cls.cotisations_vente_1 = cotisations.Vente.objects.create( + facture=cls.cotisations_facture_1, # Dep cotisations.Facture + number=2, + name="cotisations_vente_1", + prix=10, + duration=1, + type_cotisation=cotisations.Vente.COTISATION_TYPE[0][0] + ) + # A cotisation is automatically created by the Vente object and + # trying to create another cotisation associated with this vente + # will fail so we simply retrieve it so it can be used in the tests + cls.cotisations_cotisation_1 = cotisations.Cotisation.objects.get( + vente=cls.cotisations_vente_1, # Dep cotisations.Vente + ) + cls.machines_machine_1 = machines.Machine.objects.create( + user=cls.users_user_1, # Dep users.User + name="machines_machine_1", + active=True + ) + cls.machines_ouvertureportlist_1 = machines.OuverturePortList.objects.create( + name="machines_ouvertureportlist_1" + ) + cls.machines_soa_1 = machines.SOA.objects.create( + name="machines_soa_1", + mail="postmaster@example.net", + refresh=86400, + retry=7200, + expire=3600000, + ttl=172800 + ) + cls.machines_extension_1 = machines.Extension.objects.create( + name="machines_extension_1", + need_infra=False, + # Do not set origin because of circular dependency + origin_v6="2001:db8:1234::", + soa=cls.machines_soa_1 # Dep machines.SOA + ) + cls.machines_vlan_1 = machines.Vlan.objects.create( + vlan_id=0, + name="machines_vlan_1", + comment="machines Vlan 1" + ) + cls.machines_iptype_1 = machines.IpType.objects.create( + type="machines_iptype_1", + extension=cls.machines_extension_1, # Dep machines.Extension + need_infra=False, + domaine_ip_start="10.0.0.1", + domaine_ip_stop="10.0.0.255", + prefix_v6="2001:db8:1234::", + vlan=cls.machines_vlan_1, # Dep machines.Vlan + ouverture_ports=cls.machines_ouvertureportlist_1 # Dep machines.OuverturePortList + ) + # All IPs in the IpType range are autocreated so we can't create + # new ones and thus we only retrieve it if needed in the tests + cls.machines_iplist_1 = machines.IpList.objects.get( + ipv4="10.0.0.1", + ip_type=cls.machines_iptype_1, # Dep machines.IpType + ) + cls.machines_machinetype_1 = machines.MachineType.objects.create( + type="machines_machinetype_1", + ip_type=cls.machines_iptype_1, # Dep machines.IpType + ) + cls.machines_interface_1 = machines.Interface.objects.create( + ipv4=cls.machines_iplist_1, # Dep machines.IpList + mac_address="00:00:00:00:00:00", + machine=cls.machines_machine_1, # Dep machines.Machine + type=cls.machines_machinetype_1, # Dep machines.MachineType + details="machines Interface 1", + #port_lists=[cls.machines_ouvertureportlist_1] # Dep machines.OuverturePortList + ) + cls.machines_domain_1 = machines.Domain.objects.create( + interface_parent=cls.machines_interface_1, # Dep machines.Interface + name="machinesdomain", + extension=cls.machines_extension_1 # Dep machines.Extension + # Do no define cname for circular dependency + ) + cls.machines_mx_1 = machines.Mx.objects.create( + zone=cls.machines_extension_1, # Dep machines.Extension + priority=10, + name=cls.machines_domain_1 # Dep machines.Domain + ) + cls.machines_ns_1 = machines.Ns.objects.create( + zone=cls.machines_extension_1, # Dep machines.Extension + ns=cls.machines_domain_1 # Dep machines.Domain + ) + cls.machines_txt_1 = machines.Txt.objects.create( + zone=cls.machines_extension_1, # Dep machines.Extension + field1="machines_txt_1", + field2="machies Txt 1" + ) + cls.machines_srv_1 = machines.Srv.objects.create( + service="machines_srv_1", + protocole=machines.Srv.TCP, + extension=cls.machines_extension_1, # Dep machines.Extension + ttl=172800, + priority=0, + port=1, + target=cls.machines_domain_1, # Dep machines.Domain + ) + cls.machines_ipv6list_1 = machines.Ipv6List.objects.create( + ipv6="2001:db8:1234::", + interface=cls.machines_interface_1, # Dep machines.Interface + slaac_ip=False + ) + cls.machines_service_1 = machines.Service.objects.create( + service_type="machines_service_1", + min_time_regen=datetime.timedelta(minutes=1), + regular_time_regen=datetime.timedelta(hours=1) + # Do not define service_link because circular dependency + ) + cls.machines_servicelink_1 = machines.Service_link.objects.create( + service=cls.machines_service_1, # Dep machines.Service + server=cls.machines_interface_1, # Dep machines.Interface + last_regen=datetime.datetime.now(datetime.timezone.utc), + asked_regen=False + ) + cls.machines_ouvertureport_1 = machines.OuverturePort.objects.create( + begin=1, + end=2, + port_list=cls.machines_ouvertureportlist_1, # Dep machines.OuverturePortList + protocole=machines.OuverturePort.TCP, + io=machines.OuverturePort.OUT + ) + cls.machines_nas_1 = machines.Nas.objects.create( + name="machines_nas_1", + nas_type=cls.machines_machinetype_1, # Dep machines.MachineType + machine_type=cls.machines_machinetype_1, # Dep machines.MachineType + port_access_mode=machines.Nas.AUTH[0][0], + autocapture_mac=False + ) + cls.preferences_service_1 = preferences.Service.objects.create( + name="preferences_service_1", + url="https://example.net", + description="preferences Service 1", + image="/media/logo/none.png" + ) + cls.topologie_stack_1 = topologie.Stack.objects.create( + name="topologie_stack_1", + stack_id="1", + details="topologie Stack 1", + member_id_min=1, + member_id_max=10 + ) + cls.topologie_accespoint_1 = topologie.AccessPoint.objects.create( + user=cls.users_user_1, # Dep users.User + name="machines_machine_1", + active=True, + location="topologie AccessPoint 1" + ) + cls.topologie_server_1 = topologie.Server.objects.create( + user=cls.users_user_1, # Dep users.User + name="machines_machine_1", + active=True + ) + cls.topologie_building_1 = topologie.Building.objects.create( + name="topologie_building_1" + ) + cls.topologie_switchbay_1 = topologie.SwitchBay.objects.create( + name="topologie_switchbay_1", + building=cls.topologie_building_1, # Dep topologie.Building + info="topologie SwitchBay 1" + ) + cls.topologie_constructorswitch_1 = topologie.ConstructorSwitch.objects.create( + name="topologie_constructorswitch_1" + ) + cls.topologie_modelswitch_1 = topologie.ModelSwitch.objects.create( + reference="topologie_modelswitch_1", + constructor=cls.topologie_constructorswitch_1 # Dep topologie.ConstructorSwitch + ) + cls.topologie_switch_1 = topologie.Switch.objects.create( + user=cls.users_user_1, # Dep users.User + name="machines_machine_1", + active=True, + number=10, + stack=cls.topologie_stack_1, # Dep topologie.Stack + stack_member_id=1, + model=cls.topologie_modelswitch_1, # Dep topologie.ModelSwitch + switchbay=cls.topologie_switchbay_1 # Dep topologie.SwitchBay + ) + cls.topologie_room_1 = topologie.Room.objects.create( + name="topologie_romm_1", + details="topologie Room 1" + ) + cls.topologie_port_1 = topologie.Port.objects.create( + switch=cls.topologie_switch_1, # Dep topologie.Switch + port=1, + room=cls.topologie_room_1, # Dep topologie.Room + radius=topologie.Port.STATES[0][0], + vlan_force=cls.machines_vlan_1, # Dep machines.Vlan + details="topologie_switch_1" + ) + cls.topologie_port_2 = topologie.Port.objects.create( + switch=cls.topologie_switch_1, # Dep topologie.Switch + port=2, + machine_interface=cls.machines_interface_1, # Dep machines.Interface + radius=topologie.Port.STATES[0][0], + vlan_force=cls.machines_vlan_1, # Dep machines.Vlan + details="topologie_switch_1" + ) + cls.topologie_port_3 = topologie.Port.objects.create( + switch=cls.topologie_switch_1, # Dep topologie.Switch + port=3, + room=cls.topologie_room_1, # Dep topologie.Room + radius=topologie.Port.STATES[0][0], + # Do not defines related because circular dependency # Dep machines.Vlan + details="topologie_switch_1" + ) + cls.users_ban_1 = users.Ban.objects.create( + user=cls.users_user_1, # Dep users.User + raison="users Ban 1", + date_start=datetime.datetime.now(datetime.timezone.utc), + date_end=datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1), + state=users.Ban.STATES[0][0] + ) + cls.users_club_1 = users.Club.objects.create( + password="password", + last_login=datetime.datetime.now(datetime.timezone.utc), + surname="users_club_1", + pseudo="usersclub1", + email="users_club_1@example.net", + school=cls.users_school_1, # Dep users.School + shell=cls.users_listshell_1, # Dep users.ListShell + comment="users Club 1 comment", + pwd_ntlm="", + state=users.User.STATES[0][0], + registered=datetime.datetime.now(datetime.timezone.utc), + telephone="0123456789", + uid_number=21103, + rezo_rez_uid=21103 + ) +# Need merge of MR145 to work +# TODO: Merge !145 +# cls.users_listright_1 = users.ListRight.objects.create( +# unix_name="userslistright", +# gid=601, +# critical=False, +# details="userslistright" +# ) + cls.users_serviceuser_1 = users.ServiceUser.objects.create( + password="password", + last_login=datetime.datetime.now(datetime.timezone.utc), + pseudo="usersserviceuser1", + access_group=users.ServiceUser.ACCESS[0][0], + comment="users ServiceUser 1" + ) + cls.users_whitelist_1 = users.Whitelist.objects.create( + user=cls.users_user_1, + raison="users Whitelist 1", + date_start=datetime.datetime.now(datetime.timezone.utc), + date_end=datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1) + ) + + 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. + + 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 is not None: + assert_more(response, url, format) + + def test_no_auth_endpoints_with_no_auth(self): + """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 = self.no_auth_endpoints + self.check_responses_code(urls, codes.ok) + + def test_auth_endpoints_with_no_auth(self): + """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 = self.auth_no_perm_endpoints + self.auth_perm_endpoints + self.check_responses_code(urls, codes.unauthorized) + + def test_no_auth_endpoints_with_auth(self): + """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 = self.no_auth_endpoints + self.check_responses_code(urls, codes.ok) + + def test_auth_no_perm_endpoints_with_auth_and_no_perm(self): + """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 = self.auth_no_perm_endpoints + self.check_responses_code(urls, codes.ok) + + def test_auth_perm_endpoints_with_auth_and_no_perm(self): + """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 = self.auth_perm_endpoints + self.check_responses_code(urls, codes.forbidden) + + def test_auth_endpoints_with_auth_and_perm(self): + """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 = self.auth_no_perm_endpoints + self.auth_perm_endpoints + self.check_responses_code(urls, codes.ok) + + def test_endpoints_not_found(self): + """Tests that every endpoint that uses a primary key parameter, + returns a Not Found (404) response when queried with non-existing + 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' + urls = self.not_found_endpoints + self.check_responses_code(urls, codes.not_found) + + def test_formats(self): + """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) + + urls = self.no_auth_endpoints + self.auth_no_perm_endpoints + \ + self.auth_perm_endpoints + + def assert_more(response, url, format): + """Assert the response is valid json when format is json""" + if format is 'json': + json.loads(response.content.decode()) + + self.check_responses_code(urls, codes.ok, + formats=[None, 'json', 'api'], + 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/article/', + '/api/cotisations/banque/', + '/api/cotisations/cotisation/', + '/api/cotisations/facture/', + '/api/cotisations/paiement/', + '/api/cotisations/vente/', + '/api/machines/domain/', + '/api/machines/extension/', + '/api/machines/interface/', + '/api/machines/iplist/', + '/api/machines/iptype/', + '/api/machines/ipv6list/', + '/api/machines/machine/', + '/api/machines/machinetype/', + '/api/machines/mx/', + '/api/machines/nas/', + '/api/machines/ns/', + '/api/machines/ouvertureportlist/', + '/api/machines/ouvertureport/', + '/api/machines/servicelink/', + '/api/machines/service/', + '/api/machines/soa/', + '/api/machines/srv/', + '/api/machines/txt/', + '/api/machines/vlan/', + '/api/preferences/service/', + '/api/topologie/acesspoint/', + '/api/topologie/building/', + '/api/topologie/constructorswitch/', + '/api/topologie/modelswitch/', + '/api/topologie/room/', + '/api/topologie/server/', + '/api/topologie/stack/', + '/api/topologie/switch/', + '/api/topologie/switchbay/', + '/api/topologie/switchport/', + '/api/users/adherent/', + '/api/users/ban/', + '/api/users/club/', + '/api/users/listright/', + '/api/users/school/', + '/api/users/serviceuser/', + '/api/users/shell/', + '/api/users/user/', + '/api/users/whitelist/', + '/api/dns/zones/', + '/api/dhcp/hostmacip/', + '/api/mailing/standard', + '/api/mailing/club', + '/api/services/regen/', + ] + superuser = None + + @classmethod + def setUpTestData(cls): + # A user with all the rights + # We need to use a different username than for the first + # test case because TestCase is using rollbacks which don't + # trigger the ldap_sync() thus the LDAP still have data about + # the old users. + cls.superuser = users.User.objects.create_superuser( + "apisuperuser2", + "apisuperuser2", + "apisuperuser2@example.net", + "apisuperuser2" + ) + + @classmethod + def tearDownClass(cls): + cls.superuser.delete() + super().tearDownClass() + + def test_pagination(self): + """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: + with self.subTest(url=url): + response = self.client.get(url, format='json') + res_json = json.loads(response.content.decode()) + assert 'count' in res_json.keys() + assert 'next' in res_json.keys() + assert 'previous' in res_json.keys() + assert 'results' in res_json.keys() + assert not len('results') > 100 -# Create your tests here. diff --git a/api/urls.py b/api/urls.py index 9b77f308..942435dd 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,55 +17,92 @@ # 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 +from django.conf.urls import url, include from . import views +from .routers import AllViewsRouter + + +router = AllViewsRouter() +# COTISATIONS +router.register_viewset(r'cotisations/facture', views.FactureViewSet) +router.register_viewset(r'cotisations/vente', views.VenteViewSet) +router.register_viewset(r'cotisations/article', views.ArticleViewSet) +router.register_viewset(r'cotisations/banque', views.BanqueViewSet) +router.register_viewset(r'cotisations/paiement', views.PaiementViewSet) +router.register_viewset(r'cotisations/cotisation', views.CotisationViewSet) +# MACHINES +router.register_viewset(r'machines/machine', views.MachineViewSet) +router.register_viewset(r'machines/machinetype', views.MachineTypeViewSet) +router.register_viewset(r'machines/iptype', views.IpTypeViewSet) +router.register_viewset(r'machines/vlan', views.VlanViewSet) +router.register_viewset(r'machines/nas', views.NasViewSet) +router.register_viewset(r'machines/soa', views.SOAViewSet) +router.register_viewset(r'machines/extension', views.ExtensionViewSet) +router.register_viewset(r'machines/mx', views.MxViewSet) +router.register_viewset(r'machines/ns', views.NsViewSet) +router.register_viewset(r'machines/txt', views.TxtViewSet) +router.register_viewset(r'machines/srv', views.SrvViewSet) +router.register_viewset(r'machines/interface', views.InterfaceViewSet) +router.register_viewset(r'machines/ipv6list', views.Ipv6ListViewSet) +router.register_viewset(r'machines/domain', views.DomainViewSet) +router.register_viewset(r'machines/iplist', views.IpListViewSet) +router.register_viewset(r'machines/service', views.ServiceViewSet) +router.register_viewset(r'machines/servicelink', views.ServiceLinkViewSet, base_name='servicelink') +router.register_viewset(r'machines/ouvertureportlist', views.OuverturePortListViewSet) +router.register_viewset(r'machines/ouvertureport', views.OuverturePortViewSet) +# PREFERENCES +router.register_view(r'preferences/optionaluser', views.OptionalUserView), +router.register_view(r'preferences/optionalmachine', views.OptionalMachineView), +router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView), +router.register_view(r'preferences/generaloption', views.GeneralOptionView), +router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'), +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 +router.register_viewset(r'topologie/stack', views.StackViewSet) +router.register_viewset(r'topologie/acesspoint', views.AccessPointViewSet) +router.register_viewset(r'topologie/switch', views.SwitchViewSet) +router.register_viewset(r'topologie/server', views.ServerViewSet) +router.register_viewset(r'topologie/modelswitch', views.ModelSwitchViewSet) +router.register_viewset(r'topologie/constructorswitch', views.ConstructorSwitchViewSet) +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 +router.register_viewset(r'users/user', views.UserViewSet) +router.register_viewset(r'users/club', views.ClubViewSet) +router.register_viewset(r'users/adherent', views.AdherentViewSet) +router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet) +router.register_viewset(r'users/school', views.SchoolViewSet) +router.register_viewset(r'users/listright', views.ListRightViewSet) +router.register_viewset(r'users/shell', views.ShellViewSet, base_name='shell') +router.register_viewset(r'users/ban', views.BanViewSet) +router.register_viewset(r'users/whitelist', views.WhitelistViewSet) +# SERVICE REGEN +router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name='serviceregen') +# DHCP +router.register_view(r'dhcp/hostmacip', views.HostMacIpView), +# DNS +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 AUTHENTICATION +router.register_view(r'token-auth', views.ObtainExpiringAuthToken) 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), - - # Unifi controler AP names - url(r'^unifi/ap_names/$', views.accesspoint_ip_dns), - - # 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 - ), + url(r'^', include(router.urls)), ] diff --git a/api/utils.py b/api/utils.py deleted file mode 100644 index d65cff44..00000000 --- a/api/utils.py +++ /dev/null @@ -1,123 +0,0 @@ -# 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): - """The decorator to use on a specific view - """ - def wrapper(request, *args, **kwargs): - """The wrapper used for a specific request - """ - 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 index eda4dd59..8fffe606 100644 --- a/api/views.py +++ b/api/views.py @@ -18,600 +18,544 @@ # 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, ... """ -from django.contrib.auth.decorators import login_required, permission_required -from django.views.decorators.csrf import csrf_exempt +import datetime -from re2o.utils import ( - all_has_access, - all_active_assigned_interfaces, - filter_active_interfaces -) -from users.models import Club -from machines.models import ( - Service_link, - Service, - Interface, - Domain, - IpType, - Mx, - Ns, - Txt, - Srv, - Extension, - OuverturePortList, - OuverturePort -) +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 +from rest_framework import viewsets, generics, views -from .serializers import ( - ServicesSerializer, - ServiceLinkSerializer, - FullInterfaceSerializer, - DomainSerializer, - TypeSerializer, - MxSerializer, - NsSerializer, - TxtSerializer, - SrvSerializer, - ExtensionSerializer, - InterfaceSerializer, - MailingMemberSerializer, - MailingSerializer -) -from .utils import JSONError, JSONSuccess, accept_method +import cotisations.models as cotisations +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 +from .pagination import PageSizedPagination +from .permissions import ACLPermission -@csrf_exempt -@login_required -@permission_required('machines.serveur') -@accept_method(['GET']) -def services(_request): - """The list of the different services and servers couples +# COTISATIONS - 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 ? + +class FactureViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `cotisations.models.Facture` objects. """ - - service_link = (Service_link.objects.all() - .select_related('server__domain') - .select_related('service')) - seria = ServicesSerializer(service_link, many=True) - return JSONSuccess(seria.data) + queryset = cotisations.Facture.objects.all() + serializer_class = serializers.FactureSerializer -@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. +class VenteViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `cotisations.models.Vente` objects. """ + queryset = cotisations.Vente.objects.all() + serializer_class = serializers.VenteSerializer - 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) + +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 + + +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 +# 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 + + def get_object(self): + return preferences.OptionalUser.objects.first() + + +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 + + def get_object(self): + return preferences.OptionalMachine.objects.first() + + +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 + + def get_object(self): + return preferences.OptionalTopologie.objects.first() + + +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 + + def get_object(self): + return preferences.GeneralOption.objects.first() + + +class HomeServiceViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `preferences.models.Service` objects. + """ + queryset = preferences.Service.objects.all() + serializer_class = serializers.HomeServiceSerializer + + +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 + + def get_object(self): + return preferences.AssoOption.objects.first() + + +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 + + def get_object(self): + return preferences.HomeOption.objects.first() + + +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 + + def get_object(self): + return preferences.MailMessageOption.objects.first() + + +# 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 ServerViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.Server` objects. + """ + queryset = topologie.Server.objects.all() + serializer_class = serializers.ServerSerializer + + +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 + + +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 + + +# SERVICE REGEN + + +class ServiceRegenViewSet(viewsets.ModelViewSet): + """Exposes list and details of the services to regen + """ + serializer_class = serializers.ServiceRegenSerializer + + def get_queryset(self): + queryset = machines.Service_link.objects.select_related( + 'server__domain' + ).select_related( + 'service' ) - ) - 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() + if 'hostname' in self.request.GET: + hostname = self.request.GET['hostname'] + queryset = queryset.filter(server__domain__name__iexact=hostname) + return queryset -@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 +# DHCP - 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 +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 - query = Service_link.objects.filter( - server__in=Interface.objects.filter( - domain__in=Domain.objects.filter(name=server_name) + +# 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 + .prefetch_related('soa') + .prefetch_related('ns_set').prefetch_related('ns_set__ns') + .prefetch_related('origin') + .prefetch_related('mx_set').prefetch_related('mx_set__name') + .prefetch_related('txt_set') + .prefetch_related('srv_set').prefetch_related('srv_set__target') + .all()) + serializer_class = serializers.DNSZonesSerializer + + +# 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]} + + def get(self, request, format=None): + adherents_data = serializers.MailingMemberSerializer(all_has_access(), many=True).data + data = [{'name': 'adherents', 'members': adherents_data}] + paginator = self.pagination_class() + paginator.paginate_queryset(data, request) + return paginator.get_paginated_response(data) + + +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 + + +# 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) + user = serializer.validated_data['user'] + token, created = Token.objects.get_or_create(user=user) + + token_duration = datetime.timedelta( + seconds=settings.API_TOKEN_DURATION ) - ) - if not query: - return JSONError("This service is not active for this server") + utc_now = datetime.datetime.now(datetime.timezone.utc) + if not created and token.created < utc_now - token_duration: + token.delete() + token = Token.objects.create(user=user) + token.created = datetime.datetime.utcnow() + token.save() - services_objects = query.all() - seria = ServiceLinkSerializer(services_objects, 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 accesspoint_ip_dns(_request): - """The list of all active interfaces with all the associated infos - (MAC, IP, IpType, DNS name and associated zone extension) - - Only display access points. Use to gen unifi controler names - - 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) - .filter(machine__accesspoint__isnull=False)) - seria = FullInterfaceSerializer(interfaces, 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, ml_name): - """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, ml_name): - """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) + return Response({ + 'token': token.key, + 'expiration': token.created + token_duration + }) diff --git a/install_utils/apache2/re2o-tls.conf b/install_utils/apache2/re2o-tls.conf index 83e2cf13..eb8f2c42 100644 --- a/install_utils/apache2/re2o-tls.conf +++ b/install_utils/apache2/re2o-tls.conf @@ -26,6 +26,7 @@ WSGIScriptAlias / PATH/re2o/wsgi.py WSGIProcessGroup re2o WSGIDaemonProcess re2o processes=2 threads=16 maximum-requests=1000 display-name=re2o + WSGIPassAuthorization On SSLCertificateFile /etc/letsencrypt/live/LE_PATH/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/LE_PATH/privkey.pem diff --git a/install_utils/apache2/re2o.conf b/install_utils/apache2/re2o.conf index 680fb05d..1b4e02b3 100644 --- a/install_utils/apache2/re2o.conf +++ b/install_utils/apache2/re2o.conf @@ -19,5 +19,6 @@ WSGIScriptAlias / PATH/re2o/wsgi.py WSGIProcessGroup re2o WSGIDaemonProcess re2o processes=2 threads=16 maximum-requests=1000 display-name=re2o + WSGIPassAuthorization On diff --git a/machines/migrations/0082_auto_20180525_2209.py b/machines/migrations/0082_auto_20180525_2209.py new file mode 100644 index 00000000..1da2370c --- /dev/null +++ b/machines/migrations/0082_auto_20180525_2209.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-05-25 20:09 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0081_auto_20180521_1413'), + ] + + operations = [ + migrations.AlterModelOptions( + name='service_link', + options={'permissions': (('view_service_link', 'Peut voir un objet service_link'),)}, + ), + ] diff --git a/machines/models.py b/machines/models.py index fe1923bb..fd4999d6 100644 --- a/machines/models.py +++ b/machines/models.py @@ -562,6 +562,25 @@ class Extension(RevMixin, AclMixin, models.Model): entry += "@ IN AAAA " + str(self.origin_v6) return entry + def get_associated_a_records(self): + from re2o.utils import all_active_assigned_interfaces + return (all_active_assigned_interfaces() + .filter(type__ip_type__extension=self) + .filter(ipv4__isnull=False)) + + def get_associated_aaaa_records(self): + from re2o.utils import all_active_interfaces + return (all_active_interfaces(full=True) + .filter(type__ip_type__extension=self)) + + def get_associated_cname_records(self): + from re2o.utils import all_active_assigned_interfaces + return (Domain.objects + .filter(extension=self) + .filter(cname__isnull=False) + .filter(interface_parent__in=all_active_assigned_interfaces()) + .prefetch_related('cname')) + @staticmethod def can_use_all(user_request, *_args, **_kwargs): """Superdroit qui permet d'utiliser toutes les extensions sans @@ -1388,12 +1407,18 @@ class Service_link(RevMixin, AclMixin, models.Model): last_regen = models.DateTimeField(auto_now_add=True) asked_regen = models.BooleanField(default=False) + class Meta: + permissions = ( + ("view_service_link", "Peut voir un objet service_link"), + ) + def done_regen(self): """ Appellé lorsqu'un serveur a regénéré son service""" self.last_regen = timezone.now() self.asked_regen = False self.save() + @property def need_regen(self): """ Décide si le temps minimal écoulé est suffisant pour provoquer une régénération de service""" @@ -1406,6 +1431,19 @@ class Service_link(RevMixin, AclMixin, models.Model): ) < timezone.now() ) + @need_regen.setter + def need_regen(self, value): + """ + Force to set the need_regen value. True means a regen is asked and False + means a regen has been done. + + :param value: (bool) The value to set to + """ + if not value: + self.last_regen = timezone.now() + self.asked_regen = value + self.save() + def __str__(self): return str(self.server) + " " + str(self.service) diff --git a/machines/serializers.py b/machines/serializers.py index 9476e9d0..f3a47c55 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -376,7 +376,7 @@ class ServiceServersSerializer(serializers.ModelSerializer): @staticmethod def get_regen_status(obj): """ The string representation of the regen status """ - return obj.need_regen() + return obj.need_regen class OuverturePortsSerializer(serializers.Serializer): diff --git a/pip_dev_requirements.txt b/pip_dev_requirements.txt new file mode 100644 index 00000000..cabe8473 --- /dev/null +++ b/pip_dev_requirements.txt @@ -0,0 +1,2 @@ +-r pip_requirements.txt +volatildap diff --git a/re2o/settings.py b/re2o/settings.py index 24226817..7c119a55 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -75,7 +75,6 @@ LOCAL_APPS = ( 're2o', 'preferences', 'logs', - 'api', ) INSTALLED_APPS = ( DJANGO_CONTRIB_APPS + @@ -174,3 +173,8 @@ GRAPH_MODELS = { 'all_applications': True, 'group_models': True, } + +# Activate API +if 'api' in INSTALLED_APPS: + from api.settings import * + INSTALLED_APPS += API_APPS diff --git a/re2o/settings_local.example.py b/re2o/settings_local.example.py index e15455df..662c1447 100644 --- a/re2o/settings_local.example.py +++ b/re2o/settings_local.example.py @@ -56,6 +56,10 @@ DATABASES = { 'USER': 'db_user_value', 'PASSWORD': DB_PASSWORD, 'HOST': 'db_host_value', + 'TEST': { + 'CHARSET': 'utf8', + 'COLLATION': 'utf8_general_ci' + } }, 'ldap': { # The LDAP 'ENGINE': 'ldapdb.backends.ldap', diff --git a/re2o/urls.py b/re2o/urls.py index 47172521..34cb0b15 100644 --- a/re2o/urls.py +++ b/re2o/urls.py @@ -42,6 +42,7 @@ Including another URLconf """ from __future__ import unicode_literals +from django.conf import settings from django.conf.urls import include, url from django.contrib import admin from django.contrib.auth import views as auth_views @@ -70,6 +71,8 @@ urlpatterns = [ r'^preferences/', include('preferences.urls', namespace='preferences') ), - url(r'^api/', include('api.urls', namespace='api')), - ] +if 'api' in settings.INSTALLED_APPS: + urlpatterns += [ + url(r'^api/', include('api.urls', namespace='api')), + ] diff --git a/test_utils/__init__.py b/test_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test_utils/ldap/schema/radius.schema b/test_utils/ldap/schema/radius.schema new file mode 100644 index 00000000..cee7502a --- /dev/null +++ b/test_utils/ldap/schema/radius.schema @@ -0,0 +1,564 @@ +# This is a LDAPv3 schema for RADIUS attributes. +# Tested on OpenLDAP 2.0.7 +# Posted by Javier Fernandez-Sanguino Pena +# LDAP v3 version by Jochen Friedrich +# Updates by Adrian Pavlykevych +############## + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.1 + NAME 'radiusArapFeatures' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.2 + NAME 'radiusArapSecurity' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.3 + NAME 'radiusArapZoneAccess' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.44 + NAME 'radiusAuthType' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.4 + NAME 'radiusCallbackId' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.5 + NAME 'radiusCallbackNumber' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.6 + NAME 'radiusCalledStationId' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.7 + NAME 'radiusCallingStationId' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.8 + NAME 'radiusClass' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.45 + NAME 'radiusClientIPAddress' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.9 + NAME 'radiusFilterId' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.10 + NAME 'radiusFramedAppleTalkLink' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.11 + NAME 'radiusFramedAppleTalkNetwork' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.12 + NAME 'radiusFramedAppleTalkZone' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.13 + NAME 'radiusFramedCompression' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.14 + NAME 'radiusFramedIPAddress' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.15 + NAME 'radiusFramedIPNetmask' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.16 + NAME 'radiusFramedIPXNetwork' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.17 + NAME 'radiusFramedMTU' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.18 + NAME 'radiusFramedProtocol' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.19 + NAME 'radiusFramedRoute' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.20 + NAME 'radiusFramedRouting' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.46 + NAME 'radiusGroupName' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.47 + NAME 'radiusHint' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.48 + NAME 'radiusHuntgroupName' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.21 + NAME 'radiusIdleTimeout' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.22 + NAME 'radiusLoginIPHost' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.23 + NAME 'radiusLoginLATGroup' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.24 + NAME 'radiusLoginLATNode' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.25 + NAME 'radiusLoginLATPort' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.26 + NAME 'radiusLoginLATService' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.27 + NAME 'radiusLoginService' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.28 + NAME 'radiusLoginTCPPort' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.29 + NAME 'radiusPasswordRetry' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.30 + NAME 'radiusPortLimit' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.49 + NAME 'radiusProfileDn' + DESC '' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.31 + NAME 'radiusPrompt' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.50 + NAME 'radiusProxyToRealm' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.51 + NAME 'radiusReplicateToRealm' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.52 + NAME 'radiusRealm' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.32 + NAME 'radiusServiceType' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.33 + NAME 'radiusSessionTimeout' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.34 + NAME 'radiusTerminationAction' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.35 + NAME 'radiusTunnelAssignmentId' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.36 + NAME 'radiusTunnelMediumType' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.37 + NAME 'radiusTunnelPassword' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.38 + NAME 'radiusTunnelPreference' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.39 + NAME 'radiusTunnelPrivateGroupId' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.40 + NAME 'radiusTunnelServerEndpoint' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.41 + NAME 'radiusTunnelType' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.42 + NAME 'radiusVSA' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.43 + NAME 'radiusTunnelClientEndpoint' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + + +#need to change asn1.id +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.53 + NAME 'radiusSimultaneousUse' + DESC '' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.54 + NAME 'radiusLoginTime' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.55 + NAME 'radiusUserCategory' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.56 + NAME 'radiusStripUserName' + DESC '' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.57 + NAME 'dialupAccess' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.58 + NAME 'radiusExpiration' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.59 + NAME 'radiusCheckItem' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + +attributetype + ( 1.3.6.1.4.1.3317.4.3.1.60 + NAME 'radiusReplyItem' + DESC '' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + ) + + +objectclass + ( 1.3.6.1.4.1.3317.4.3.2.1 + NAME 'radiusprofile' + SUP top AUXILIARY + DESC '' + MUST cn + MAY ( radiusArapFeatures $ radiusArapSecurity $ radiusArapZoneAccess $ + radiusAuthType $ radiusCallbackId $ radiusCallbackNumber $ + radiusCalledStationId $ radiusCallingStationId $ radiusClass $ + radiusClientIPAddress $ radiusFilterId $ radiusFramedAppleTalkLink $ + radiusFramedAppleTalkNetwork $ radiusFramedAppleTalkZone $ + radiusFramedCompression $ radiusFramedIPAddress $ + radiusFramedIPNetmask $ radiusFramedIPXNetwork $ + radiusFramedMTU $ radiusFramedProtocol $ + radiusCheckItem $ radiusReplyItem $ + radiusFramedRoute $ radiusFramedRouting $ radiusIdleTimeout $ + radiusGroupName $ radiusHint $ radiusHuntgroupName $ + radiusLoginIPHost $ radiusLoginLATGroup $ radiusLoginLATNode $ + radiusLoginLATPort $ radiusLoginLATService $ radiusLoginService $ + radiusLoginTCPPort $ radiusLoginTime $ radiusPasswordRetry $ + radiusPortLimit $ radiusPrompt $ radiusProxyToRealm $ + radiusRealm $ radiusReplicateToRealm $ radiusServiceType $ + radiusSessionTimeout $ radiusStripUserName $ + radiusTerminationAction $ radiusTunnelClientEndpoint $ radiusProfileDn $ + radiusSimultaneousUse $ radiusTunnelAssignmentId $ + radiusTunnelMediumType $ radiusTunnelPassword $ radiusTunnelPreference $ + radiusTunnelPrivateGroupId $ radiusTunnelServerEndpoint $ + radiusTunnelType $ radiusUserCategory $ radiusVSA $ + radiusExpiration $ dialupAccess ) + ) diff --git a/test_utils/ldap/schema/samba.schema b/test_utils/ldap/schema/samba.schema new file mode 100644 index 00000000..08173119 --- /dev/null +++ b/test_utils/ldap/schema/samba.schema @@ -0,0 +1,644 @@ +## +## schema file for OpenLDAP 2.x +## Schema for storing Samba user accounts and group maps in LDAP +## OIDs are owned by the Samba Team +## +## Prerequisite schemas - uid (cosine.schema) +## - displayName (inetorgperson.schema) +## - gidNumber (nis.schema) +## +## 1.3.6.1.4.1.7165.2.1.x - attributetypes +## 1.3.6.1.4.1.7165.2.2.x - objectclasses +## +## Printer support +## 1.3.6.1.4.1.7165.2.3.1.x - attributetypes +## 1.3.6.1.4.1.7165.2.3.2.x - objectclasses +## +## Samba4 +## 1.3.6.1.4.1.7165.4.1.x - attributetypes +## 1.3.6.1.4.1.7165.4.2.x - objectclasses +## 1.3.6.1.4.1.7165.4.3.x - LDB/LDAP Controls +## 1.3.6.1.4.1.7165.4.4.x - LDB/LDAP Extended Operations +## 1.3.6.1.4.1.7165.4.255.x - mapped OIDs due to conflicts between AD and standards-track +## +## External projects +## 1.3.6.1.4.1.7165.655.x +## 1.3.6.1.4.1.7165.655.1.x - GSS-NTLMSSP +## +## ----- READ THIS WHEN ADDING A NEW ATTRIBUTE OR OBJECT CLASS ------ +## +## Run the 'get_next_oid' bash script in this directory to find the +## next available OID for attribute type and object classes. +## +## $ ./get_next_oid +## attributetype ( 1.3.6.1.4.1.7165.2.1.XX NAME .... +## objectclass ( 1.3.6.1.4.1.7165.2.2.XX NAME .... +## +## Also ensure that new entries adhere to the declaration style +## used throughout this file +## +## ( 1.3.6.1.4.1.7165.2.XX.XX NAME .... +## ^ ^ ^ +## +## The spaces are required for the get_next_oid script (and for +## readability). +## +## ------------------------------------------------------------------ + +# objectIdentifier SambaRoot 1.3.6.1.4.1.7165 +# objectIdentifier Samba3 SambaRoot:2 +# objectIdentifier Samba3Attrib Samba3:1 +# objectIdentifier Samba3ObjectClass Samba3:2 +# objectIdentifier Samba4 SambaRoot:4 + +######################################################################## +## HISTORICAL ## +######################################################################## + +## +## Password hashes +## +#attributetype ( 1.3.6.1.4.1.7165.2.1.1 NAME 'lmPassword' +# DESC 'LanManager Passwd' +# EQUALITY caseIgnoreIA5Match +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.2 NAME 'ntPassword' +# DESC 'NT Passwd' +# EQUALITY caseIgnoreIA5Match +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} SINGLE-VALUE ) + +## +## Account flags in string format ([UWDX ]) +## +#attributetype ( 1.3.6.1.4.1.7165.2.1.4 NAME 'acctFlags' +# DESC 'Account Flags' +# EQUALITY caseIgnoreIA5Match +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{16} SINGLE-VALUE ) + +## +## Password timestamps & policies +## +#attributetype ( 1.3.6.1.4.1.7165.2.1.3 NAME 'pwdLastSet' +# DESC 'NT pwdLastSet' +# EQUALITY integerMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.5 NAME 'logonTime' +# DESC 'NT logonTime' +# EQUALITY integerMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.6 NAME 'logoffTime' +# DESC 'NT logoffTime' +# EQUALITY integerMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.7 NAME 'kickoffTime' +# DESC 'NT kickoffTime' +# EQUALITY integerMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.8 NAME 'pwdCanChange' +# DESC 'NT pwdCanChange' +# EQUALITY integerMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.9 NAME 'pwdMustChange' +# DESC 'NT pwdMustChange' +# EQUALITY integerMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +## +## string settings +## +#attributetype ( 1.3.6.1.4.1.7165.2.1.10 NAME 'homeDrive' +# DESC 'NT homeDrive' +# EQUALITY caseIgnoreIA5Match +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{4} SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.11 NAME 'scriptPath' +# DESC 'NT scriptPath' +# EQUALITY caseIgnoreIA5Match +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{255} SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.12 NAME 'profilePath' +# DESC 'NT profilePath' +# EQUALITY caseIgnoreIA5Match +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{255} SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.13 NAME 'userWorkstations' +# DESC 'userWorkstations' +# EQUALITY caseIgnoreIA5Match +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{255} SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.17 NAME 'smbHome' +# DESC 'smbHome' +# EQUALITY caseIgnoreIA5Match +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.18 NAME 'domain' +# DESC 'Windows NT domain to which the user belongs' +# EQUALITY caseIgnoreIA5Match +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} ) + +## +## user and group RID +## +#attributetype ( 1.3.6.1.4.1.7165.2.1.14 NAME 'rid' +# DESC 'NT rid' +# EQUALITY integerMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +#attributetype ( 1.3.6.1.4.1.7165.2.1.15 NAME 'primaryGroupID' +# DESC 'NT Group RID' +# EQUALITY integerMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +## +## The smbPasswordEntry objectclass has been depreciated in favor of the +## sambaAccount objectclass +## +#objectclass ( 1.3.6.1.4.1.7165.2.2.1 NAME 'smbPasswordEntry' SUP top AUXILIARY +# DESC 'Samba smbpasswd entry' +# MUST ( uid $ uidNumber ) +# MAY ( lmPassword $ ntPassword $ pwdLastSet $ acctFlags )) + +#objectclass ( 1.3.6.1.4.1.7165.2.2.2 NAME 'sambaAccount' SUP top STRUCTURAL +# DESC 'Samba Account' +# MUST ( uid $ rid ) +# MAY ( cn $ lmPassword $ ntPassword $ pwdLastSet $ logonTime $ +# logoffTime $ kickoffTime $ pwdCanChange $ pwdMustChange $ acctFlags $ +# displayName $ smbHome $ homeDrive $ scriptPath $ profilePath $ +# description $ userWorkstations $ primaryGroupID $ domain )) + +#objectclass ( 1.3.6.1.4.1.7165.2.2.3 NAME 'sambaAccount' SUP top AUXILIARY +# DESC 'Samba Auxiliary Account' +# MUST ( uid $ rid ) +# MAY ( cn $ lmPassword $ ntPassword $ pwdLastSet $ logonTime $ +# logoffTime $ kickoffTime $ pwdCanChange $ pwdMustChange $ acctFlags $ +# displayName $ smbHome $ homeDrive $ scriptPath $ profilePath $ +# description $ userWorkstations $ primaryGroupID $ domain )) + +######################################################################## +## END OF HISTORICAL ## +######################################################################## + +####################################################################### +## Attributes used by Samba 3.0 schema ## +####################################################################### + +## +## Password hashes +## +attributetype ( 1.3.6.1.4.1.7165.2.1.24 NAME 'sambaLMPassword' + DESC 'LanManager Password' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.25 NAME 'sambaNTPassword' + DESC 'MD4 hash of the unicode password' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} SINGLE-VALUE ) + +## +## Account flags in string format ([UWDX ]) +## +attributetype ( 1.3.6.1.4.1.7165.2.1.26 NAME 'sambaAcctFlags' + DESC 'Account Flags' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{16} SINGLE-VALUE ) + +## +## Password timestamps & policies +## +attributetype ( 1.3.6.1.4.1.7165.2.1.27 NAME 'sambaPwdLastSet' + DESC 'Timestamp of the last password update' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.28 NAME 'sambaPwdCanChange' + DESC 'Timestamp of when the user is allowed to update the password' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.29 NAME 'sambaPwdMustChange' + DESC 'Timestamp of when the password will expire' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.30 NAME 'sambaLogonTime' + DESC 'Timestamp of last logon' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.31 NAME 'sambaLogoffTime' + DESC 'Timestamp of last logoff' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.32 NAME 'sambaKickoffTime' + DESC 'Timestamp of when the user will be logged off automatically' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.48 NAME 'sambaBadPasswordCount' + DESC 'Bad password attempt count' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.49 NAME 'sambaBadPasswordTime' + DESC 'Time of the last bad password attempt' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.55 NAME 'sambaLogonHours' + DESC 'Logon Hours' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{42} SINGLE-VALUE ) + +## +## string settings +## +attributetype ( 1.3.6.1.4.1.7165.2.1.33 NAME 'sambaHomeDrive' + DESC 'Driver letter of home directory mapping' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{4} SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.34 NAME 'sambaLogonScript' + DESC 'Logon script path' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.35 NAME 'sambaProfilePath' + DESC 'Roaming profile path' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.36 NAME 'sambaUserWorkstations' + DESC 'List of user workstations the user is allowed to logon to' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.37 NAME 'sambaHomePath' + DESC 'Home directory UNC path' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.38 NAME 'sambaDomainName' + DESC 'Windows NT domain to which the user belongs' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.47 NAME 'sambaMungedDial' + DESC 'Base64 encoded user parameter string' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1050} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.54 NAME 'sambaPasswordHistory' + DESC 'Concatenated MD5 hashes of the salted NT passwords used on this account' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} ) + +## +## SID, of any type +## + +attributetype ( 1.3.6.1.4.1.7165.2.1.20 NAME 'sambaSID' + DESC 'Security ID' + EQUALITY caseIgnoreIA5Match + SUBSTR caseExactIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} SINGLE-VALUE ) + +## +## Primary group SID, compatible with ntSid +## + +attributetype ( 1.3.6.1.4.1.7165.2.1.23 NAME 'sambaPrimaryGroupSID' + DESC 'Primary Group Security ID' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.51 NAME 'sambaSIDList' + DESC 'Security ID List' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} ) + +## +## group mapping attributes +## +attributetype ( 1.3.6.1.4.1.7165.2.1.19 NAME 'sambaGroupType' + DESC 'NT Group Type' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +## +## Store info on the domain +## + +attributetype ( 1.3.6.1.4.1.7165.2.1.21 NAME 'sambaNextUserRid' + DESC 'Next NT rid to give our for users' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.22 NAME 'sambaNextGroupRid' + DESC 'Next NT rid to give out for groups' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.39 NAME 'sambaNextRid' + DESC 'Next NT rid to give out for anything' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.40 NAME 'sambaAlgorithmicRidBase' + DESC 'Base at which the samba RID generation algorithm should operate' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.41 NAME 'sambaShareName' + DESC 'Share Name' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.42 NAME 'sambaOptionName' + DESC 'Option Name' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.43 NAME 'sambaBoolOption' + DESC 'A boolean option' + EQUALITY booleanMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.44 NAME 'sambaIntegerOption' + DESC 'An integer option' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.45 NAME 'sambaStringOption' + DESC 'A string option' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.46 NAME 'sambaStringListOption' + DESC 'A string list option' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + + +##attributetype ( 1.3.6.1.4.1.7165.2.1.50 NAME 'sambaPrivName' +## SUP name ) + +##attributetype ( 1.3.6.1.4.1.7165.2.1.52 NAME 'sambaPrivilegeList' +## DESC 'Privileges List' +## EQUALITY caseIgnoreIA5Match +## SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.53 NAME 'sambaTrustFlags' + DESC 'Trust Password Flags' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +# "min password length" +attributetype ( 1.3.6.1.4.1.7165.2.1.58 NAME 'sambaMinPwdLength' + DESC 'Minimal password length (default: 5)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# "password history" +attributetype ( 1.3.6.1.4.1.7165.2.1.59 NAME 'sambaPwdHistoryLength' + DESC 'Length of Password History Entries (default: 0 => off)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# "user must logon to change password" +attributetype ( 1.3.6.1.4.1.7165.2.1.60 NAME 'sambaLogonToChgPwd' + DESC 'Force Users to logon for password change (default: 0 => off, 2 => on)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# "maximum password age" +attributetype ( 1.3.6.1.4.1.7165.2.1.61 NAME 'sambaMaxPwdAge' + DESC 'Maximum password age, in seconds (default: -1 => never expire passwords)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# "minimum password age" +attributetype ( 1.3.6.1.4.1.7165.2.1.62 NAME 'sambaMinPwdAge' + DESC 'Minimum password age, in seconds (default: 0 => allow immediate password change)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# "lockout duration" +attributetype ( 1.3.6.1.4.1.7165.2.1.63 NAME 'sambaLockoutDuration' + DESC 'Lockout duration in minutes (default: 30, -1 => forever)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# "reset count minutes" +attributetype ( 1.3.6.1.4.1.7165.2.1.64 NAME 'sambaLockoutObservationWindow' + DESC 'Reset time after lockout in minutes (default: 30)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# "bad lockout attempt" +attributetype ( 1.3.6.1.4.1.7165.2.1.65 NAME 'sambaLockoutThreshold' + DESC 'Lockout users after bad logon attempts (default: 0 => off)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# "disconnect time" +attributetype ( 1.3.6.1.4.1.7165.2.1.66 NAME 'sambaForceLogoff' + DESC 'Disconnect Users outside logon hours (default: -1 => off, 0 => on)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# "refuse machine password change" +attributetype ( 1.3.6.1.4.1.7165.2.1.67 NAME 'sambaRefuseMachinePwdChange' + DESC 'Allow Machine Password changes (default: 0 => off)' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +# +attributetype ( 1.3.6.1.4.1.7165.2.1.68 NAME 'sambaClearTextPassword' + DESC 'Clear text password (used for trusted domain passwords)' + EQUALITY octetStringMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) + +# +attributetype ( 1.3.6.1.4.1.7165.2.1.69 NAME 'sambaPreviousClearTextPassword' + DESC 'Previous clear text password (used for trusted domain passwords)' + EQUALITY octetStringMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.70 NAME 'sambaTrustType' + DESC 'Type of trust' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.71 NAME 'sambaTrustAttributes' + DESC 'Trust attributes for a trusted domain' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.72 NAME 'sambaTrustDirection' + DESC 'Direction of a trust' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.73 NAME 'sambaTrustPartner' + DESC 'Fully qualified name of the domain with which a trust exists' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.74 NAME 'sambaFlatName' + DESC 'NetBIOS name of a domain' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.75 NAME 'sambaTrustAuthOutgoing' + DESC 'Authentication information for the outgoing portion of a trust' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1050} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.76 NAME 'sambaTrustAuthIncoming' + DESC 'Authentication information for the incoming portion of a trust' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1050} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.77 NAME 'sambaSecurityIdentifier' + DESC 'SID of a trusted domain' + EQUALITY caseIgnoreIA5Match SUBSTR caseExactIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.78 NAME 'sambaTrustForestTrustInfo' + DESC 'Forest trust information for a trusted domain object' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1050} ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.79 NAME 'sambaTrustPosixOffset' + DESC 'POSIX offset of a trust' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.4.1.7165.2.1.80 NAME 'sambaSupportedEncryptionTypes' + DESC 'Supported encryption types of a trust' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) + +####################################################################### +## objectClasses used by Samba 3.0 schema ## +####################################################################### + +## The X.500 data model (and therefore LDAPv3) says that each entry can +## only have one structural objectclass. OpenLDAP 2.0 does not enforce +## this currently but will in v2.1 + +## +## added new objectclass (and OID) for 3.0 to help us deal with backwards +## compatibility with 2.2 installations (e.g. ldapsam_compat) --jerry +## +objectclass ( 1.3.6.1.4.1.7165.2.2.6 NAME 'sambaSamAccount' SUP top AUXILIARY + DESC 'Samba 3.0 Auxilary SAM Account' + MUST ( uid $ sambaSID ) + MAY ( cn $ sambaLMPassword $ sambaNTPassword $ sambaPwdLastSet $ + sambaLogonTime $ sambaLogoffTime $ sambaKickoffTime $ + sambaPwdCanChange $ sambaPwdMustChange $ sambaAcctFlags $ + displayName $ sambaHomePath $ sambaHomeDrive $ sambaLogonScript $ + sambaProfilePath $ description $ sambaUserWorkstations $ + sambaPrimaryGroupSID $ sambaDomainName $ sambaMungedDial $ + sambaBadPasswordCount $ sambaBadPasswordTime $ + sambaPasswordHistory $ sambaLogonHours)) + +## +## Group mapping info +## +objectclass ( 1.3.6.1.4.1.7165.2.2.4 NAME 'sambaGroupMapping' SUP top AUXILIARY + DESC 'Samba Group Mapping' + MUST ( gidNumber $ sambaSID $ sambaGroupType ) + MAY ( displayName $ description $ sambaSIDList )) + +## +## Trust password for trust relationships (any kind) +## +objectclass ( 1.3.6.1.4.1.7165.2.2.14 NAME 'sambaTrustPassword' SUP top STRUCTURAL + DESC 'Samba Trust Password' + MUST ( sambaDomainName $ sambaNTPassword $ sambaTrustFlags ) + MAY ( sambaSID $ sambaPwdLastSet )) + +## +## Trust password for trusted domains +## (to be stored beneath the trusting sambaDomain object in the DIT) +## +objectclass ( 1.3.6.1.4.1.7165.2.2.15 NAME 'sambaTrustedDomainPassword' SUP top STRUCTURAL + DESC 'Samba Trusted Domain Password' + MUST ( sambaDomainName $ sambaSID $ + sambaClearTextPassword $ sambaPwdLastSet ) + MAY ( sambaPreviousClearTextPassword )) + +## +## Whole-of-domain info +## +objectclass ( 1.3.6.1.4.1.7165.2.2.5 NAME 'sambaDomain' SUP top STRUCTURAL + DESC 'Samba Domain Information' + MUST ( sambaDomainName $ + sambaSID ) + MAY ( sambaNextRid $ sambaNextGroupRid $ sambaNextUserRid $ + sambaAlgorithmicRidBase $ + sambaMinPwdLength $ sambaPwdHistoryLength $ sambaLogonToChgPwd $ + sambaMaxPwdAge $ sambaMinPwdAge $ + sambaLockoutDuration $ sambaLockoutObservationWindow $ sambaLockoutThreshold $ + sambaForceLogoff $ sambaRefuseMachinePwdChange )) + +## +## used for idmap_ldap module +## +objectclass ( 1.3.6.1.4.1.7165.2.2.7 NAME 'sambaUnixIdPool' SUP top AUXILIARY + DESC 'Pool for allocating UNIX uids/gids' + MUST ( uidNumber $ gidNumber ) ) + + +objectclass ( 1.3.6.1.4.1.7165.2.2.8 NAME 'sambaIdmapEntry' SUP top AUXILIARY + DESC 'Mapping from a SID to an ID' + MUST ( sambaSID ) + MAY ( uidNumber $ gidNumber ) ) + +objectclass ( 1.3.6.1.4.1.7165.2.2.9 NAME 'sambaSidEntry' SUP top STRUCTURAL + DESC 'Structural Class for a SID' + MUST ( sambaSID ) ) + +objectclass ( 1.3.6.1.4.1.7165.2.2.10 NAME 'sambaConfig' SUP top AUXILIARY + DESC 'Samba Configuration Section' + MAY ( description ) ) + +objectclass ( 1.3.6.1.4.1.7165.2.2.11 NAME 'sambaShare' SUP top STRUCTURAL + DESC 'Samba Share Section' + MUST ( sambaShareName ) + MAY ( description ) ) + +objectclass ( 1.3.6.1.4.1.7165.2.2.12 NAME 'sambaConfigOption' SUP top STRUCTURAL + DESC 'Samba Configuration Option' + MUST ( sambaOptionName ) + MAY ( sambaBoolOption $ sambaIntegerOption $ sambaStringOption $ + sambaStringListoption $ description ) ) + + +## retired during privilege rewrite +##objectclass ( 1.3.6.1.4.1.7165.2.2.13 NAME 'sambaPrivilege' SUP top AUXILIARY +## DESC 'Samba Privilege' +## MUST ( sambaSID ) +## MAY ( sambaPrivilegeList ) ) + +## +## used for IPA_ldapsam +## +objectclass ( 1.3.6.1.4.1.7165.2.2.16 NAME 'sambaTrustedDomain' SUP top STRUCTURAL + DESC 'Samba Trusted Domain Object' + MUST ( cn ) + MAY ( sambaTrustType $ sambaTrustAttributes $ sambaTrustDirection $ + sambaTrustPartner $ sambaFlatName $ sambaTrustAuthOutgoing $ + sambaTrustAuthIncoming $ sambaSecurityIdentifier $ + sambaTrustForestTrustInfo $ sambaTrustPosixOffset $ + sambaSupportedEncryptionTypes) ) diff --git a/test_utils/runner.py b/test_utils/runner.py new file mode 100644 index 00000000..54ddd82f --- /dev/null +++ b/test_utils/runner.py @@ -0,0 +1,166 @@ +# 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. + +"""Defines the custom runners for Re2o. +""" + +import volatildap +import os.path + +from django.test.runner import DiscoverRunner +from django.conf import settings + +from users.models import LdapUser, LdapUserGroup, LdapServiceUser, LdapServiceUserGroup + +# The path of this file +__here = os.path.dirname(os.path.realpath(__file__)) +# The absolute path where to find the schemas for the LDAP +schema_path = os.path.abspath(os.path.join(__here, 'ldap', 'schema')) +# The absolute path of the "radius.schema" file +radius_schema_path = os.path.join(schema_path, 'radius.schema') +# The absolute path of the "samba.schema" file +samba_schema_path = os.path.join(schema_path, 'samba.schema') + +# The suffix for the LDAP +suffix = 'dc=example,dc=net' +# The admin CN of the LDAP +rootdn = 'cn=admin,'+suffix + +# Defines all ldap_entry mandatory for Re2o under a key-value list format +# that can be used directly by volatildap. For more on how to generate this +# data, see https://gitlab.federez.net/re2o/scripts/blob/master/print_ldap_entries.py +ldapentry_Utilisateurs = ('cn=Utilisateurs,'+suffix, { + 'cn': ['Utilisateurs'], + 'sambaSID': ['500'], + 'uid': ['Users'], + 'objectClass': ['posixGroup', 'top', 'sambaSamAccount', 'radiusprofile'], + 'gidNumber': ['500'], +}) +ldapentry_groups = ('ou=groups,'+suffix, { + 'ou': ['groups'], + 'objectClass': ['organizationalUnit'], + 'description': ["Groupes d'utilisateurs"], +}) +ldapentry_services = ('ou=services,ou=groups,'+suffix, { + 'ou': ['services'], + 'objectClass': ['organizationalUnit'], + 'description': ['Groupes de comptes techniques'], +}) +ldapentry_service_users = ('ou=service-users,'+suffix, { + 'ou': ['service-users'], + 'objectClass': ['organizationalUnit'], + 'description': ["Utilisateurs techniques de l'annuaire"], +}) +ldapentry_freeradius = ('cn=freeradius,ou=service-users,'+suffix, { + 'cn': ['freeradius'], + 'objectClass': ['applicationProcess', 'simpleSecurityObject'], + 'userPassword': ['FILL_IT'], +}) +ldapentry_nssauth = ('cn=nssauth,ou=service-users,'+suffix, { + 'cn': ['nssauth'], + 'objectClass': ['applicationProcess', 'simpleSecurityObject'], + 'userPassword': ['FILL_IT'], +}) +ldapentry_auth = ('cn=auth,ou=services,ou=groups,'+suffix, { + 'cn': ['auth'], + 'objectClass': ['groupOfNames'], + 'member': ['cn=nssauth,ou=service-users,'+suffix], +}) +ldapentry_posix = ('ou=posix,ou=groups,'+suffix, { + 'ou': ['posix'], + 'objectClass': ['organizationalUnit'], + 'description': ['Groupes de comptes POSIX'], +}) +ldapentry_wifi = ('cn=wifi,ou=service-users,'+suffix, { + 'cn': ['wifi'], + 'objectClass': ['applicationProcess', 'simpleSecurityObject'], + 'userPassword': ['FILL_IT'], +}) +ldapentry_usermgmt = ('cn=usermgmt,ou=services,ou=groups,'+suffix, { + 'cn': ['usermgmt'], + 'objectClass': ['groupOfNames'], + 'member': ['cn=wifi,ou=service-users,'+suffix], +}) +ldapentry_replica = ('cn=replica,ou=service-users,'+suffix, { + 'cn': ['replica'], + 'objectClass': ['applicationProcess', 'simpleSecurityObject'], + 'userPassword': ['FILL_IT'], +}) +ldapentry_readonly = ('cn=readonly,ou=services,ou=groups,'+suffix, { + 'cn': ['readonly'], + 'objectClass': ['groupOfNames'], + 'member': ['cn=replica,ou=service-users,'+suffix, 'cn=freeradius,ou=service-users,'+suffix], +}) +ldapbasic = dict([ldapentry_Utilisateurs, ldapentry_groups, + ldapentry_services, ldapentry_service_users, + ldapentry_freeradius, ldapentry_nssauth, ldapentry_auth, + ldapentry_posix, ldapentry_wifi, ldapentry_usermgmt, + ldapentry_replica, ldapentry_readonly]) + + +class DiscoverLdapRunner(DiscoverRunner): + """Discovers all the tests in the project + + This is a simple subclass of the default test runner + `django.test.runner.DiscoverRunner` that creates a test LDAP + right after the test databases are setup and destroys it right + before the test databases are setup. + It also ensure re2o's settings are using this new LDAP. + """ + + # The `volatildap.LdapServer` instance initiated with the minimal + # structure required by Re2o + ldap_server = volatildap.LdapServer( + suffix=suffix, + rootdn=rootdn, + initial_data=ldapbasic, + schemas=['core.schema', 'cosine.schema', 'inetorgperson.schema', + 'nis.schema', radius_schema_path, samba_schema_path] + ) + + def __init__(self, *args, **kwargs): + settings.DATABASES['ldap']['USER'] = self.ldap_server.rootdn + settings.DATABASES['ldap']['PASSWORD'] = self.ldap_server.rootpw + settings.DATABASES['ldap']['NAME'] = self.ldap_server.uri + settings.LDAP['base_user_dn'] = ldapentry_Utilisateurs[0] + settings.LDAP['base_userservice_dn'] = ldapentry_service_users[0] + settings.LDAP['base_usergroup_dn'] = ldapentry_posix[0] + settings.LDAP['base_userservicegroup_dn'] = ldapentry_services[0] + settings.LDAP['user_gid'] = ldapentry_Utilisateurs[1].get('gidNumber', ["500"])[0] + LdapUser.base_dn = settings.LDAP['base_user_dn'] + LdapUserGroup.base_dn = settings.LDAP['base_usergroup_dn'] + LdapServiceUser.base_dn = settings.LDAP['base_userservice_dn'] + LdapServiceUserGroup.base_dn = settings.LDAP['base_userservicegroup_dn'] + super(DiscoverLdapRunner, self).__init__(*args, **kwargs) + + + def setup_databases(self, *args, **kwargs): + ret = super(DiscoverLdapRunner, self).setup_databases(*args, **kwargs) + print("Creating test LDAP with volatildap...") + self.ldap_server.start() + return ret + + def teardown_databases(self, *args, **kwargs): + self.ldap_server.stop() + print("Destroying test LDAP...") + super(DiscoverLdapRunner, self).teardown_databases(*args, **kwargs) + diff --git a/users/tests.py b/users/tests.py index 85a8e9f1..6b2bfb41 100644 --- a/users/tests.py +++ b/users/tests.py @@ -23,6 +23,65 @@ The tests for the Users module. """ -# from django.test import TestCase +import os.path + +from django.test import TestCase +from django.conf import settings +from . import models + +import volatildap + + +class SchoolTestCase(TestCase): + def test_school_are_created(self): + s = models.School.objects.create(name="My awesome school") + self.assertEqual(s.name, "My awesome school") + + +class ListShellTestCase(TestCase): + def test_shell_are_created(self): + s = models.ListShell.objects.create(shell="/bin/zsh") + self.assertEqual(s.shell, "/bin/zsh") + + +class LdapUserTestCase(TestCase): + def test_create_ldap_user(self): + g = models.LdapUser.objects.create( + gid="500", + name="users_test_ldapuser", + uid="users_test_ldapuser", + uidNumber="21001", + sn="users_test_ldapuser", + login_shell="/bin/false", + mail="user@example.net", + given_name="users_test_ldapuser", + home_directory="/home/moamoak", + display_name="users_test_ldapuser", + dialupAccess="False", + sambaSID="21001", + user_password="{SSHA}aBcDeFgHiJkLmNoPqRsTuVwXyZ012345", + sambat_nt_password="0123456789ABCDEF0123456789ABCDEF", + macs=[], + shadowexpire="0" + ) + self.assertEqual(g.name, 'users_test_ldapuser') + + +class LdapUserGroupTestCase(TestCase): + def test_create_ldap_user_group(self): + g = models.LdapUserGroup.objects.create( + gid="501", + members=[], + name="users_test_ldapusergroup" + ) + self.assertEqual(g.name, 'users_test_ldapusergroup') + + +class LdapServiceUserTestCase(TestCase): + def test_create_ldap_service_user(self): + g = models.LdapServiceUser.objects.create( + name="users_test_ldapserviceuser", + user_password="{SSHA}AbCdEfGhIjKlMnOpQrStUvWxYz987654" + ) + self.assertEqual(g.name, 'users_test_ldapserviceuser') -# Create your tests here.