From cdfcfd04768cdb35592f0fe96441cd3e1be78d11 Mon Sep 17 00:00:00 2001 From: Benjamin Pereto Date: Mon, 11 Jan 2021 21:25:25 +0100 Subject: [PATCH] FEATURE: REST API (#34) * FEATURE: REST API * REFACTOR: disable nexted write * FEATURE: add owner filter - wip * FEATURE: add repo events and statistics - wip * FIX: add repo size unit to statistic * FEATURE: add openapi schema * FEATURE: update requirements * FEATURE: API * FIX: update deps * STYLE: fix pep * FIX: move migrations * FEATURE: bump release Co-authored-by: Benjamin Pereto --- .pylintrc | 4 +- borg/requirements.in | 2 + borg/requirements.txt | 12 +- docker-compose.dev.yml | 2 +- docker-compose.yml | 10 +- requirements-dev.txt | 37 +----- requirements.in | 19 +++ requirements.txt | 94 +++++++------ src/api/__init__.py | 1 + src/api/apps.py | 8 ++ src/api/lib/__init__.py | 0 src/api/lib/serializers.py | 94 +++++++++++++ src/api/lib/viewsets.py | 92 +++++++++++++ src/api/migrations/__init__.py | 0 src/api/router.py | 5 + src/api/serializers/__init__.py | 3 + src/api/serializers/key.py | 29 ++++ src/api/serializers/repo.py | 84 ++++++++++++ src/api/serializers/user.py | 24 ++++ src/api/templates/rest_framework/api.html | 5 + src/api/tests/__init__.py | 0 src/api/tests/test_key.py | 138 ++++++++++++++++++++ src/api/tests/test_repo.py | 39 ++++++ src/api/urls/__init__.py | 1 + src/api/urls/base.py | 8 ++ src/api/urls/schema.py | 14 ++ src/api/views/__init__.py | 3 + src/api/views/key.py | 29 ++++ src/api/views/repo.py | 101 ++++++++++++++ src/api/views/user.py | 29 ++++ src/borghive/lib/validators.py | 3 +- src/borghive/managers.py | 16 +++ src/borghive/migrations/0002_append_only.py | 2 +- src/borghive/migrations/0004_stats_unit.py | 20 +++ src/borghive/models/base.py | 1 + src/borghive/models/key.py | 3 + src/borghive/models/repository.py | 6 +- src/borghive/tests/test_key.py | 4 +- src/borghive/views/repository.py | 2 +- src/core/settings.py | 18 ++- src/core/urls.py | 1 + 41 files changed, 865 insertions(+), 98 deletions(-) create mode 100644 borg/requirements.in create mode 100644 requirements.in create mode 100644 src/api/__init__.py create mode 100644 src/api/apps.py create mode 100644 src/api/lib/__init__.py create mode 100644 src/api/lib/serializers.py create mode 100644 src/api/lib/viewsets.py create mode 100644 src/api/migrations/__init__.py create mode 100644 src/api/router.py create mode 100644 src/api/serializers/__init__.py create mode 100644 src/api/serializers/key.py create mode 100644 src/api/serializers/repo.py create mode 100644 src/api/serializers/user.py create mode 100644 src/api/templates/rest_framework/api.html create mode 100644 src/api/tests/__init__.py create mode 100644 src/api/tests/test_key.py create mode 100644 src/api/tests/test_repo.py create mode 100644 src/api/urls/__init__.py create mode 100644 src/api/urls/base.py create mode 100644 src/api/urls/schema.py create mode 100644 src/api/views/__init__.py create mode 100644 src/api/views/key.py create mode 100644 src/api/views/repo.py create mode 100644 src/api/views/user.py create mode 100644 src/borghive/managers.py create mode 100644 src/borghive/migrations/0004_stats_unit.py diff --git a/.pylintrc b/.pylintrc index df06e52..a6ff6b8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -33,6 +33,7 @@ limit-inference-results=100 # List of plugins (as comma separated values of python module names) to load, # usually to register additional checkers. load-plugins=pylint_django +django-settings-module=core.settings # Pickle collected data for later comparisons. persistent=yes @@ -144,7 +145,8 @@ disable=print-statement, wildcard-import, missing-module-docstring, invalid-name, - too-many-ancestors + too-many-ancestors, + imported-auth-user # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/borg/requirements.in b/borg/requirements.in new file mode 100644 index 0000000..e7d1434 --- /dev/null +++ b/borg/requirements.in @@ -0,0 +1,2 @@ +borgbackup==1.1.14 +mysql-connector==2.2.9 diff --git a/borg/requirements.txt b/borg/requirements.txt index 2c99a0e..1906adf 100644 --- a/borg/requirements.txt +++ b/borg/requirements.txt @@ -2,13 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile borg/requirements.txt +# pip-compile # - -# install from github until issue https://github.com/bpereto/borg-hive/issues/19 is resolved by borgbackup release 1.1.14 -git+https://github.com/borgbackup/borg@c53190176389dcbd711060a8a9f4326e66d1d533#egg=borgbackup - -environs==7.4.0 # via -r borg/requirements.txt -marshmallow==3.5.2 # via environs -mysql-connector==2.2.9 # via -r borg/requirements.txt -python-dotenv==0.13.0 # via environs +borgbackup==1.1.14 # via -r requirements.in +mysql-connector==2.2.9 # via -r requirements.in diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3b3cea8..dac442c 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -39,7 +39,7 @@ services: build: context: . dockerfile: Dockerfile.base - entrypoint: celery worker -A core -l DEBUG -B --scheduler django_celery_beat.schedulers:DatabaseScheduler + entrypoint: celery -A core worker -l DEBUG -B --scheduler django_celery_beat.schedulers:DatabaseScheduler environment: - DEBUG=True - MYSQL_HOST=db diff --git a/docker-compose.yml b/docker-compose.yml index 66975a9..86acd83 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: - .env app: - image: bpereto/borg-hive:0.1.2 + image: bpereto/borg-hive:0.1.3 env_file: - .env volumes: @@ -23,8 +23,8 @@ services: - db worker: - image: bpereto/borg-hive:0.1.2 - entrypoint: celery worker -A core -l INFO -B --scheduler django_celery_beat.schedulers:DatabaseScheduler + image: bpereto/borg-hive:0.1.3 + entrypoint: celery -A core worker -l INFO -B --scheduler django_celery_beat.schedulers:DatabaseScheduler env_file: - .env volumes: @@ -34,7 +34,7 @@ services: - db watcher: - image: bpereto/borg-hive:0.1.2 + image: bpereto/borg-hive:0.1.3 entrypoint: /bin/bash -c "/app/manage.py watch_repositories" env_file: - .env @@ -45,7 +45,7 @@ services: restart: 'on-failure' borg: - image: bpereto/borg-hive:borg-0.1.2 + image: bpereto/borg-hive:borg-0.1.3 depends_on: - db env_file: diff --git a/requirements-dev.txt b/requirements-dev.txt index f68e665..e40d6ab 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,31 +1,6 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile requirements-dev.txt -# -astroid==2.4.1 # via -r requirements-dev.txt, pylint -certifi==2020.4.5.1 # via -r requirements-dev.txt -chardet==3.0.4 # via -r requirements-dev.txt -commonmark==0.9.1 # via -r requirements-dev.txt -coverage==5.1 # via -r requirements-dev.txt -docutils==0.16 # via -r requirements-dev.txt -idna==2.9 # via -r requirements-dev.txt -isort==4.3.21 # via -r requirements-dev.txt, pylint -lazy-object-proxy==1.4.3 # via -r requirements-dev.txt, astroid -markupsafe==1.1.1 # via -r requirements-dev.txt -mccabe==0.6.1 # via -r requirements-dev.txt, pylama, pylint -pycodestyle==2.5.0 # via -r requirements-dev.txt, pylama -pydocstyle==5.0.2 # via -r requirements-dev.txt, pylama -pyflakes==2.2.0 # via -r requirements-dev.txt, pylama -pylama==7.7.1 # via -r requirements-dev.txt -pylint-django==2.0.15 # via -r requirements-dev.txt -pylint-plugin-utils==0.6 # via -r requirements-dev.txt, pylint-django -pylint==2.5.2 # via -r requirements-dev.txt, pylint-django, pylint-plugin-utils -pyparsing==2.4.7 # via -r requirements-dev.txt -pytz==2020.1 # via -r requirements-dev.txt -six==1.14.0 # via -r requirements-dev.txt, astroid -snowballstemmer==2.0.0 # via pydocstyle -toml==0.10.0 # via -r requirements-dev.txt, pylint -urllib3==1.25.9 # via -r requirements-dev.txt -wrapt==1.12.1 # via -r requirements-dev.txt, astroid +pylint +astroid +pylama +pylint-django +pylint-plugin-utils +coverage diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..375a3b1 --- /dev/null +++ b/requirements.in @@ -0,0 +1,19 @@ +celery==5.0.5 +django-celery-beat==2.1.0 +django-crispy-forms==1.10.0 +django-extensions==3.1.0 +django-ldapdb==1.5.1 +django-login-required-middleware==0.5.0 +django-polymorphic==3.0.0 +django==3.1.4 +djangorestframework-queryfields==1.0.0 +djangorestframework==3.12.2 +environs==9.2.0 +inotify==0.2.10 +mysqlclient==2.0.2 +pyyaml==5.3.1 +redis==3.5.3 +requests==2.25.1 +rules==2.2 +sshpubkeys==3.1.0 +uritemplate==3.0.1 diff --git a/requirements.txt b/requirements.txt index 0977234..19b6378 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,47 +2,55 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile requirements.txt +# pip-compile requirements.in # -amqp==2.5.2 # via -r requirements.txt, kombu -asgiref==3.2.7 # via -r requirements.txt, django -billiard==3.6.3.0 # via -r requirements.txt, celery -celery==4.4.2 # via -r requirements.txt, django-celery-beat -certifi==2020.4.5.1 # via -r requirements.txt, requests -cffi==1.14.0 # via -r requirements.txt, cryptography -chardet==3.0.4 # via -r requirements.txt, requests -cryptography==3.2 # via -r requirements.txt, sshpubkeys -django-celery-beat==2.0.0 # via -r requirements.txt -django-crispy-forms==1.9.0 # via -r requirements.txt -django-extensions==2.2.9 # via -r requirements.txt -django-ldapdb==1.4.0 # via -r requirements.txt -django-login-required-middleware==0.5.0 # via -r requirements.txt -django-polymorphic==2.1.2 # via -r requirements.txt -django-timezone-field==4.0 # via -r requirements.txt, django-celery-beat -django==3.0.7 # via -r requirements.txt, django-celery-beat, django-ldapdb, django-polymorphic, django-timezone-field, djangorestframework -djangorestframework==3.11.0 # via -r requirements.txt -ecdsa==0.15 # via -r requirements.txt, sshpubkeys -environs==7.4.0 # via -r requirements.txt -idna==2.9 # via -r requirements.txt, requests -inotify==0.2.10 # via -r requirements.txt -kombu==4.6.8 # via -r requirements.txt, celery -marshmallow==3.5.2 # via -r requirements.txt, environs -mysqlclient==1.4.6 # via -r requirements.txt -nose==1.3.7 # via -r requirements.txt, inotify -pyasn1-modules==0.2.8 # via -r requirements.txt, python-ldap -pyasn1==0.4.8 # via -r requirements.txt, pyasn1-modules, python-ldap -pycparser==2.20 # via -r requirements.txt, cffi -python-crontab==2.4.2 # via -r requirements.txt, django-celery-beat -python-dateutil==2.8.1 # via -r requirements.txt, python-crontab -python-dotenv==0.13.0 # via -r requirements.txt, environs -python-ldap==3.2.0 # via -r requirements.txt, django-ldapdb -pytz==2020.1 # via -r requirements.txt, celery, django, django-timezone-field -pyyaml==5.3.1 # via -r requirements.txt -redis==3.5.0 # via -r requirements.txt -requests==2.23.0 # via -r requirements.txt -rules==2.2 # via -r requirements.txt -six==1.14.0 # via -r requirements.txt, cryptography, django-extensions, ecdsa, python-dateutil -sqlparse==0.3.1 # via -r requirements.txt, django -sshpubkeys==3.1.0 # via -r requirements.txt -urllib3==1.25.9 # via -r requirements.txt, requests -vine==1.3.0 # via -r requirements.txt, amqp, celery +amqp==5.0.2 # via kombu +asgiref==3.3.1 # via django +billiard==3.6.3.0 # via celery +celery==5.0.5 # via -r requirements.in, django-celery-beat +certifi==2020.12.5 # via requests +cffi==1.14.4 # via cryptography +chardet==4.0.0 # via requests +click-didyoumean==0.0.3 # via celery +click-plugins==1.1.1 # via celery +click-repl==0.1.6 # via celery +click==7.1.2 # via celery, click-didyoumean, click-plugins, click-repl +cryptography==3.3.1 # via sshpubkeys +django-celery-beat==2.1.0 # via -r requirements.in +django-crispy-forms==1.10.0 # via -r requirements.in +django-extensions==3.1.0 # via -r requirements.in +django-ldapdb==1.5.1 # via -r requirements.in +django-login-required-middleware==0.5.0 # via -r requirements.in +django-polymorphic==3.0.0 # via -r requirements.in +django-timezone-field==4.1.1 # via django-celery-beat +django==3.1.4 # via -r requirements.in, django-celery-beat, django-ldapdb, django-polymorphic, django-timezone-field, djangorestframework +djangorestframework-queryfields==1.0.0 # via -r requirements.in +djangorestframework==3.12.2 # via -r requirements.in +ecdsa==0.16.1 # via sshpubkeys +environs==9.2.0 # via -r requirements.in +idna==2.10 # via requests +inotify==0.2.10 # via -r requirements.in +kombu==5.0.2 # via celery +marshmallow==3.10.0 # via environs +mysqlclient==2.0.2 # via -r requirements.in +nose==1.3.7 # via inotify +prompt-toolkit==3.0.8 # via click-repl +pyasn1-modules==0.2.8 # via python-ldap +pyasn1==0.4.8 # via pyasn1-modules, python-ldap +pycparser==2.20 # via cffi +python-crontab==2.5.1 # via django-celery-beat +python-dateutil==2.8.1 # via python-crontab +python-dotenv==0.15.0 # via environs +python-ldap==3.3.1 # via django-ldapdb +pytz==2020.4 # via celery, django, django-timezone-field +pyyaml==5.3.1 # via -r requirements.in +redis==3.5.3 # via -r requirements.in +requests==2.25.1 # via -r requirements.in +rules==2.2 # via -r requirements.in +six==1.15.0 # via click-repl, cryptography, ecdsa, python-dateutil +sqlparse==0.4.1 # via django +sshpubkeys==3.1.0 # via -r requirements.in +uritemplate==3.0.1 # via -r requirements.in +urllib3==1.26.2 # via requests +vine==5.0.0 # via amqp, celery +wcwidth==0.2.5 # via prompt-toolkit diff --git a/src/api/__init__.py b/src/api/__init__.py new file mode 100644 index 0000000..23ca7a6 --- /dev/null +++ b/src/api/__init__.py @@ -0,0 +1 @@ +default_app_config = 'api.apps.ApiConfig' diff --git a/src/api/apps.py b/src/api/apps.py new file mode 100644 index 0000000..f965150 --- /dev/null +++ b/src/api/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + """ + django app config for drf api + """ + name = 'api' diff --git a/src/api/lib/__init__.py b/src/api/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/api/lib/serializers.py b/src/api/lib/serializers.py new file mode 100644 index 0000000..6386559 --- /dev/null +++ b/src/api/lib/serializers.py @@ -0,0 +1,94 @@ +''' +Definition of the REST framework serializers. +''' + +__all__ = ( + 'SimpleModelSerializer', + 'SimpleHyperlinkedModelSerializer', +) + +from drf_queryfields import QueryFieldsMixin +from rest_framework.serializers import (HyperlinkedModelSerializer, + HyperlinkedRelatedField, + ModelSerializer, ReadOnlyField) +from rest_framework.utils.field_mapping import get_nested_relation_kwargs + +# pylint: disable=redefined-builtin + +NAMESPACE = 'api' + + +class SimpleModelSerializer(QueryFieldsMixin, ModelSerializer): # pylint: disable=too-few-public-methods + ''' + A REST framework `ModelSerializer` on dope, which automatically initializes + the Meta class of the serializer, based on the arguments provided in the + constructor. + ''' + namespace = NAMESPACE + + def __init__(self, *args, model=None, fields=None, **kwargs): + ''' + Initialize the Meta class before initializing the serializer. + ''' + + if not hasattr(self, 'Meta'): + self.Meta = type('Meta', (), {'model': model, 'fields': fields}) # pylint: disable=invalid-name + + super().__init__(*args, **kwargs) + + def build_url_field(self, field_name, model_class): + ''' + Create a field representing the object's own URL. + + Please note this method is overloaded to add the namespace to the view + name. + ''' + field_class, field_kwargs = super().build_url_field(field_name, model_class) + field_kwargs['view_name'] = '{}:{}'.format(self.namespace, field_kwargs['view_name']) + return field_class, field_kwargs + + def build_relational_field(self, field_name, relation_info): + field_class, field_kwargs = super().build_relational_field(field_name, relation_info) + + if hasattr(field_kwargs, 'view_name'): + field_kwargs['view_name'] = '{}:{}'.format(self.namespace, field_kwargs['view_name']) + + return field_class, field_kwargs + + +class SimpleHyperlinkedRelatedField(HyperlinkedRelatedField): + """ + A REST framework `HyperlinkedRelatedField` to support namespaces + """ + + namespace = NAMESPACE + + def __init__(self, *args, **kwargs): + kwargs['view_name'] = self.namespace + ':' + kwargs['view_name'] + super().__init__(*args, **kwargs) + + +class SimpleHyperlinkedModelSerializer(SimpleModelSerializer, HyperlinkedModelSerializer): # pylint: disable=too-few-public-methods + ''' + A REST framework `HyperlinkedModelSerializer` on dope, which automatically + initializes the Meta class of the serializer, based on the arguments + provided in the constructor. + ''' + serializer_related_field = SimpleHyperlinkedRelatedField + + id = ReadOnlyField() + + def build_nested_field(self, field_name, relation_info, nested_depth): + """ + Create nested fields for forward and reverse relationships. + """ + class NestedSerializer(SimpleHyperlinkedModelSerializer): # pylint: disable=too-few-public-methods,missing-docstring + class Meta: # pylint: disable=too-few-public-methods,missing-docstring + model = relation_info.related_model + depth = nested_depth - 1 + fields = '__all__' + + field_class = NestedSerializer + field_kwargs = get_nested_relation_kwargs(relation_info) + + return field_class, field_kwargs diff --git a/src/api/lib/viewsets.py b/src/api/lib/viewsets.py new file mode 100644 index 0000000..8034866 --- /dev/null +++ b/src/api/lib/viewsets.py @@ -0,0 +1,92 @@ +''' +Definition of the REST framework viewsets. +''' + +__all__ = ( + 'SimpleModelViewSet', + 'SimpleHyperlinkedModelViewSet', +) + +from rest_framework.viewsets import ModelViewSet + +from .serializers import SimpleModelSerializer, SimpleHyperlinkedModelSerializer + + +class SimpleModelViewSet(ModelViewSet): + ''' + A REST framework `ModelViewSet` on dope, which only requires a model to + work. + + The viewset uses a model serializer as default. + ''' + serializer_class = SimpleModelSerializer + fields = '__all__' + + @property + def queryset(self): + ''' + Return the queryset for all objects of the model. + + :return: The queryset + :rtype: django.db.query.QuerySet + ''' + return self._meta.model.objects.all() # pylint: disable=no-member + + @property + def serializer_kwargs(self): + ''' + Return the kwargs for initializing the serializer class / instance. + + :return: The keyword arguments for the serializer + :rtype: dict + ''' + if not issubclass(self.serializer_class, SimpleModelSerializer): + return {} + + return { + 'model': self.model, # pylint: disable=no-member + 'fields': self.fields, + } + + def get_serializer(self, *args, **kwargs): + ''' + Return the instantiated serializer. + + :return: The serializer instance + :rtype: rest_framework.serializers.Serializer + ''' + kwargs.update(self.serializer_kwargs) + return super().get_serializer(*args, **kwargs) + + def get_serializer_class(self): + ''' + Return the defined serializer class per viewset method. + + return a serializer class based on action + + self.viewset_serializer_class = { + 'list' : MyListSerializer, + 'create' : MyCreateSerializer + } + + :return: The serializer class + :rtype: string + ''' + + # pylint: disable=no-member + + serializer = super().get_serializer_class() + if hasattr(self, 'viewset_serializer_class') and isinstance(self.viewset_serializer_class, dict): # pylint: disable=line-too-long + if self.action in self.viewset_serializer_class.keys(): + serializer = self.viewset_serializer_class[self.action] + return serializer + + +class SimpleHyperlinkedModelViewSet(SimpleModelViewSet): + ''' + A REST framework `ModelViewSet` on dope, which only requires a model to + work. + + The viewset uses a hyperlinked model serializer as default. + ''' + serializer_class = SimpleHyperlinkedModelSerializer diff --git a/src/api/migrations/__init__.py b/src/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/api/router.py b/src/api/router.py new file mode 100644 index 0000000..91a7ede --- /dev/null +++ b/src/api/router.py @@ -0,0 +1,5 @@ +from rest_framework import routers + +router = routers.DefaultRouter() + +from api.views import * # noqa diff --git a/src/api/serializers/__init__.py b/src/api/serializers/__init__.py new file mode 100644 index 0000000..6f72e08 --- /dev/null +++ b/src/api/serializers/__init__.py @@ -0,0 +1,3 @@ +from .key import * +from .repo import * +from .user import * diff --git a/src/api/serializers/key.py b/src/api/serializers/key.py new file mode 100644 index 0000000..49dfbcb --- /dev/null +++ b/src/api/serializers/key.py @@ -0,0 +1,29 @@ +from rest_framework import serializers +from django.contrib.auth.models import Group + +from api.lib.serializers import SimpleHyperlinkedModelSerializer +from api.serializers.user import SimpleGroupSerializer, SimpleOwnerSerializer +from borghive.models import SSHPublicKey + + +class SSHPublickeySerializer(SimpleHyperlinkedModelSerializer): + """ + serializer for ssh public key + """ + + type = serializers.CharField(read_only=True) + bits = serializers.IntegerField(read_only=True) + fingerprint = serializers.CharField(read_only=True) + comment = serializers.CharField(read_only=True) + + owner = SimpleOwnerSerializer(read_only=True, default=serializers.CurrentUserDefault()) + group = SimpleGroupSerializer(many=True, read_only=True) + group_id = serializers.PrimaryKeyRelatedField(source='group', queryset=Group.objects.all(), write_only=True, many=True, required=False) + + def create(self, validated_data): + validated_data['owner'] = self.context['request'].user + return super().create(validated_data) + + class Meta: + model = SSHPublicKey + fields = '__all__' diff --git a/src/api/serializers/repo.py b/src/api/serializers/repo.py new file mode 100644 index 0000000..bc93cbd --- /dev/null +++ b/src/api/serializers/repo.py @@ -0,0 +1,84 @@ +from django.contrib.auth.models import Group +from rest_framework import serializers + +from api.lib.serializers import SimpleHyperlinkedModelSerializer +from api.serializers.key import SSHPublickeySerializer +from api.serializers.user import SimpleGroupSerializer, SimpleOwnerSerializer +from borghive.models import (Repository, RepositoryEvent, RepositoryLocation, + RepositoryStatistic, RepositoryUser) +from borghive.models.key import SSHPublicKey + + +class RepositorySerializer(SimpleHyperlinkedModelSerializer): + """ + serializer for repository + + to selectively display fields use the deifned serializers + """ + + owner = SimpleOwnerSerializer(read_only=True) + group = SimpleGroupSerializer(many=True, read_only=True) + group_id = serializers.PrimaryKeyRelatedField( + source='group', queryset=Group.objects.all(), write_only=True, many=True, required=False) + + location = SimpleHyperlinkedModelSerializer( + model=RepositoryLocation, fields='__all__', read_only=True) + location_id = serializers.PrimaryKeyRelatedField( + source='location', queryset=RepositoryLocation.objects.all(), write_only=True) + + repo_user = SimpleHyperlinkedModelSerializer( + model=RepositoryUser, fields='__all__', read_only=True) + + ssh_keys = SSHPublickeySerializer(many=True, read_only=True) + ssh_keys_id = serializers.PrimaryKeyRelatedField( + source='ssh_keys', queryset=SSHPublicKey.objects.all(), write_only=True, many=True, required=False) + + append_only_keys = SSHPublickeySerializer(many=True, read_only=True) + append_only_keys_id = serializers.PrimaryKeyRelatedField( + source='append_only_keys', queryset=SSHPublicKey.objects.all(), write_only=True, many=True, required=False) + + def create(self, validated_data, *args, **kwargs): + """ + override create method to generate repository user + and set requester as owner + """ + repo_user = RepositoryUser() + repo_user.save() + validated_data['repo_user'] = repo_user + validated_data['owner'] = self.context['request'].user + print(validated_data['owner']) + return super().create(validated_data, *args, **kwargs) + + class Meta: + model = Repository + exclude = ['last_updated', 'last_access'] + + +class SimpleRepositorySerializer(SimpleHyperlinkedModelSerializer): + """ + show only limited fields on repository + """ + + +class RepositoryEventSerializer(SimpleHyperlinkedModelSerializer): + """ + serializer for repository event + """ + + repo = RepositorySerializer(read_only=True) + + class Meta: + model = RepositoryEvent + fields = '__all__' + + +class RepositoryStatisticSerializer(SimpleHyperlinkedModelSerializer): + """ + serializer for repository event + """ + + repo = RepositorySerializer(read_only=True) + + class Meta: + model = RepositoryStatistic + fields = '__all__' diff --git a/src/api/serializers/user.py b/src/api/serializers/user.py new file mode 100644 index 0000000..0b2b954 --- /dev/null +++ b/src/api/serializers/user.py @@ -0,0 +1,24 @@ +from django.contrib.auth.models import Group, User + +from api.lib.serializers import SimpleHyperlinkedModelSerializer + + +class SimpleOwnerSerializer(SimpleHyperlinkedModelSerializer): + """ + serializer for owner aka user + limited to not expose to much information + """ + + class Meta: + model = User + fields = ('id', 'username',) + + +class SimpleGroupSerializer(SimpleHyperlinkedModelSerializer): + """ + serializer for group + """ + + class Meta: + model = Group + fields = ('id', 'name',) diff --git a/src/api/templates/rest_framework/api.html b/src/api/templates/rest_framework/api.html new file mode 100644 index 0000000..d7684d6 --- /dev/null +++ b/src/api/templates/rest_framework/api.html @@ -0,0 +1,5 @@ +{% extends "rest_framework/base.html" %} + +{% block bootstrap_theme %} + +{% endblock %} \ No newline at end of file diff --git a/src/api/tests/__init__.py b/src/api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/api/tests/test_key.py b/src/api/tests/test_key.py new file mode 100644 index 0000000..5dcafe0 --- /dev/null +++ b/src/api/tests/test_key.py @@ -0,0 +1,138 @@ +from django.contrib.auth.models import User +from django.urls import reverse +from rest_framework.test import APIClient, APITestCase + +from borghive.models import SSHPublicKey, sshpubkeys + + +class APIRSSHKeyTest(APITestCase): + + def setUp(self): + self.client = APIClient() + self.client.force_login(User.objects.get_or_create(username='admin')[0]) + + def test_get(self): + response = self.client.get(reverse('api:sshpublickey-list')) + self.assertEqual(response.status_code, 200) + + def test_create_valid_rsa_key(self): + data = { + 'name': 'ole', + 'owner_id': 2, + 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAfHetFCwshETzQ414TZkueJPGLL0IzL9beFeNMJ9UqptLQqQn0/GGfILsXsE0wg5J3B4GIO5iWE2hjEHaoUNUNZu6xU18yMrFm8MzjV6zQnubeMvG9x8CEal9/G+SmbMTpGhGjWkyVENlpcQx8OVzxkkYODKSBuQX8MiXSQ3/OTqUBSvywYIobmarfVg6CERldjfYwNI95tXSxieRaBU5w9f12X4nA6fdPAB4JXOxH8XsQVXMB5dx417PD0niPa5mVkdaJItVWIx2Z7gDdoor9nHamZY8dCfOTw8NDlF7CGe/m6J1GgokYIsNpolsmlhFyvd8IfqxXd2eJIYw+nc+UcDXp81j4E7o3T2IBD1adNE76LpEKfYW/01jRGSF0NOI1BJYP7xHz5UDVUMAsl4Sv0fbFnjJW3IPKgNFDIbdj/GRa/JnrtUa9eluzxV1bvIVOSdtsKbjmUl/MuOLl1xrRcyHjParx7hvwW8AqcwyjMkmOgRpHovPnnNNZJ1Lw8c= ole@ole' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(response.status_code, 201) + self.assertEqual(SSHPublicKey.objects.count(), 1) + + response = self.client.get(reverse('api:sshpublickey-detail', kwargs={'pk': 1})) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['owner']['id'], 1) + + def test_create_valid_ed25519_key(self): + data = { + 'name': 'key-ed25519', + 'public_key': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJy2GMJLrWk7AiHWRA8crkfxcbqGfx8mCR4/ox3C9pZe ole@ole' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(response.status_code, 201) + self.assertEqual(SSHPublicKey.objects.count(), 1) + + def test_create_valid_ecdsa_key(self): + data = { + 'name': 'key-ecdsa', + 'public_key': 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOltDNue+Pa939EWFoTJAEAbXfrD92mVsVut8TQZh4/zyQEOP5M2bK+KFbEKal9lALiGLIJbz/7tS13Td6KYqrA= ole@ole' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(response.status_code, 201) + self.assertEqual(SSHPublicKey.objects.count(), 1) + + def test_update_rsa_key(self): + self.test_create_valid_rsa_key() + + data = { + 'name': 'ole-new', + 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAfHetFCwshETzQ414TZkueJPGLL0IzL9beFeNMJ9UqptLQqQn0/GGfILsXsE0wg5J3B4GIO5iWE2hjEHaoUNUNZu6xU18yMrFm8MzjV6zQnubeMvG9x8CEal9/G+SmbMTpGhGjWkyVENlpcQx8OVzxkkYODKSBuQX8MiXSQ3/OTqUBSvywYIobmarfVg6CERldjfYwNI95tXSxieRaBU5w9f12X4nA6fdPAB4JXOxH8XsQVXMB5dx417PD0niPa5mVkdaJItVWIx2Z7gDdoor9nHamZY8dCfOTw8NDlF7CGe/m6J1GgokYIsNpolsmlhFyvd8IfqxXd2eJIYw+nc+UcDXp81j4E7o3T2IBD1adNE76LpEKfYW/01jRGSF0NOI1BJYP7xHz5UDVUMAsl4Sv0fbFnjJW3IPKgNFDIbdj/GRa/JnrtUa9eluzxV1bvIVOSdtsKbjmUl/MuOLl1xrRcyHjParx7hvwW8AqcwyjMkmOgRpHovPnnNNZJ1Lw8c= ole@oleeee' + } + response = self.client.put(reverse('api:sshpublickey-detail', kwargs={'pk': 1}), data=data) + self.assertEqual(response.status_code, 200) + self.assertEqual(SSHPublicKey.objects.count(), 1) + self.assertEqual(SSHPublicKey.objects.filter(name='ole-new').count(), 1) + + def test_update_invalid_rsa_key(self): + self.test_create_valid_rsa_key() + + key = SSHPublicKey.objects.first() + key.public_key = 'ssh-rsa AAAB3NzaC1yc2EAAAADAQABAAABgQDAfHetFCwshETzQ414TZkueJPGLL0IzL9beFeNMJ9UqptLQqQn0/GGfILsXsE0wg5J3B4GIO5iWE2hjEHaoUNUNZu6xU18yMrFm8MzjV6zQnubeMvG9x8CEal9/G+SmbMTpGhGjWkyVENlpcQx8OVzxkkYODKSBuQX8MiXSQ3/OTqUBSvywYIobmarfVg6CERldjfYwNI95tXSxieRaBU5w9f12X4nA6fdPAB4JXOxH8XsQVXMB5dx417PD0niPa5mVkdaJItVWIx2Z7gDdoor9nHamZY8dCfOTw8NDlF7CGe/m6J1GgokYIsNpolsmlhFyvd8IfqxXd2eJIYw+nc+UcDXp81j4E7o3T2IBD1adNE76LpEKfYW/01jRGSF0NOI1BJYP7xHz5UDVUMAsl4Sv0fbFnjJW3IPKgNFDIbdj/GRa/JnrtUa9eluzxV1bvIVOSdtsKbjmUl/MuOLl1xrRcyHjParx7hvwW8AqcwyjMkmOgRpHo' + with self.assertRaises(sshpubkeys.exceptions.MalformedDataError): + key.save() + + def test_create_valid_key_newline(self): + data = { + 'name': 'key-ed25519-xxx', + 'public_key': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJy2GMJLrWk7AiHWRA8crkfxcbqGfx8mCR4/ox3C9pZe asdf@asdf\n' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(SSHPublicKey.objects.count(), 1) + + def test_create_invalid_key_without_comment(self): + data = { + 'name': 'key-ed25519-x', + 'public_key': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJy2GMJLrWk7AiHWRA8crkfxcbqGfx8mCR4/ox3C9pZe' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(SSHPublicKey.objects.count(), 1) + + def test_create_invalid_key_commentspace(self): + data = { + 'name': 'key-ed25519-xx', + 'public_key': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJy2GMJLrWk7AiHWRA8crkfxcbqGfx8mCR4/ox3C9pZe asdf@asdf asdf asdf' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(SSHPublicKey.objects.count(), 0) + + def test_invalid_key(self): + data = { + 'name': 'key-ed25519-xxx', + 'public_key': 'ssh-ed25519 AAAC3NzaC1lZDI1NTE5AAAAIJy2GMJLr7AiHWRA8crkfxcbqGfx8mCR4/ox3C9pZe ole@ole' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(SSHPublicKey.objects.count(), 0) + + def test_invalid_key2(self): + data = { + 'name': 'key-ed25519-xxxx', + 'public_key': 'ssh-ed25519 AAAAC3NzaC1lZD";"a\';>sdf ole@ole' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(SSHPublicKey.objects.count(), 0) + + def test_invalid_key3(self): + data = { + 'name': 'key-asdf', + 'public_key': 'ssh-asdf AAAAC3xxxx' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(SSHPublicKey.objects.count(), 0) + + def test_invalid_key4(self): + data = { + 'name': 'key-asdf', + 'public_key': 'ssh-rsa AAAAC3xölsdflkdf$$$$$@@@$$adx' + } + response = self.client.post(reverse('api:sshpublickey-list'), data=data) + self.assertEqual(SSHPublicKey.objects.count(), 0) + + def test_invalid_update(self): + key = SSHPublicKey.objects.create(name='update', + public_key='ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAfHetFCwshETzQ414TZkueJPGLL0IzL9beFeNMJ9UqptLQqQn0/GGfILsXsE0wg5J3B4GIO5iWE2hjEHaoUNUNZu6xU18yMrFm8MzjV6zQnubeMvG9x8CEal9/G+SmbMTpGhGjWkyVENlpcQx8OVzxkkYODKSBuQX8MiXSQ3/OTqUBSvywYIobmarfVg6CERldjfYwNI95tXSxieRaBU5w9f12X4nA6fdPAB4JXOxH8XsQVXMB5dx417PD0niPa5mVkdaJItVWIx2Z7gDdoor9nHamZY8dCfOTw8NDlF7CGe/m6J1GgokYIsNpolsmlhFyvd8IfqxXd2eJIYw+nc+UcDXp81j4E7o3T2IBD1adNE76LpEKfYW/01jRGSF0NOI1BJYP7xHz5UDVUMAsl4Sv0fbFnjJW3IPKgNFDIbdj/GRa/JnrtUa9eluzxV1bvIVOSdtsKbjmUl/MuOLl1xrRcyHjParx7hvwW8AqcwyjMkmOgRpHovPnnNNZJ1Lw8c= ole@ole', + owner=User.objects.get(username='admin')) + data = { + 'name': 'update2', + 'public_key': 'ssh-rsa AAAaaaaaaaa' + } + response = self.client.put(reverse('api:sshpublickey-detail', kwargs={'pk': key.id}), data=data) + self.assertEqual(response.status_code, 400) + key.refresh_from_db() + self.assertEqual(key.name, 'update') + diff --git a/src/api/tests/test_repo.py b/src/api/tests/test_repo.py new file mode 100644 index 0000000..9d70cdb --- /dev/null +++ b/src/api/tests/test_repo.py @@ -0,0 +1,39 @@ +from django.contrib.auth.models import User +from django.urls import reverse +from rest_framework.test import APIClient, APITestCase + + +class APIRepositoryTest(APITestCase): + + def setUp(self): + self.client = APIClient() + self.client.force_login(User.objects.get_or_create(username='admin')[0]) + + def test_api_repository_list(self): + + self.test_api_repository_create() + + response = self.client.get(reverse('api:repository-list')) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()[0]['name'], 'testrepo') + + def test_api_repository_create(self): + data = { + 'name': 'testrepo', + 'ssh_keys': '2', + 'location_id': '1' + } + response = self.client.post(reverse('api:repository-list'), data=data) + self.assertEqual(response.status_code, 201) + return response.json() + + def test_api_repository_update(self): + repository = self.test_api_repository_create() + response = self.client.patch(repository['_href'], data={'alert_after_days': 3}) + self.assertEqual(response.status_code, 200) + + def test_api_repository_delete(self): + repository = self.test_api_repository_create() + + response = self.client.delete(repository['_href']) + self.assertEqual(response.status_code, 204) diff --git a/src/api/urls/__init__.py b/src/api/urls/__init__.py new file mode 100644 index 0000000..92d04e2 --- /dev/null +++ b/src/api/urls/__init__.py @@ -0,0 +1 @@ +from .base import urlpatterns diff --git a/src/api/urls/base.py b/src/api/urls/base.py new file mode 100644 index 0000000..a5814b5 --- /dev/null +++ b/src/api/urls/base.py @@ -0,0 +1,8 @@ +from django.urls import path +from django.urls import include +from api.router import router + +urlpatterns = [ + path('', include((router.urls, 'api'), namespace='api')), + path('schema/', include('api.urls.schema')) +] diff --git a/src/api/urls/schema.py b/src/api/urls/schema.py new file mode 100644 index 0000000..9b114ce --- /dev/null +++ b/src/api/urls/schema.py @@ -0,0 +1,14 @@ +from django.urls import path +from rest_framework.schemas import get_schema_view + +urlpatterns = [ + # ... + # Use the `get_schema_view()` helper to add a `SchemaView` to project URLs. + # * `title` and `description` parameters are passed to `SchemaGenerator`. + # * Provide view name for use with `reverse()`. + path('', get_schema_view( + title="Borg Hive", + description="Borg Hive API Schema", + version="1" + ), name='openapi-schema'), +] diff --git a/src/api/views/__init__.py b/src/api/views/__init__.py new file mode 100644 index 0000000..6f72e08 --- /dev/null +++ b/src/api/views/__init__.py @@ -0,0 +1,3 @@ +from .key import * +from .repo import * +from .user import * diff --git a/src/api/views/key.py b/src/api/views/key.py new file mode 100644 index 0000000..2585443 --- /dev/null +++ b/src/api/views/key.py @@ -0,0 +1,29 @@ +import logging + +from api.lib.viewsets import SimpleHyperlinkedModelViewSet +from api.router import router +from api.serializers.key import SSHPublickeySerializer +from borghive.models import SSHPublicKey + +LOGGER = logging.getLogger(__name__) + +__all__ = ['SSHPublicKeyViewSet'] + + +class SSHPublicKeyViewSet(SimpleHyperlinkedModelViewSet): + """ + SSH-Publickey Viewset + """ + queryset = SSHPublicKey.objects.all() + serializer_class = SSHPublickeySerializer + model = SSHPublicKey + + def get_queryset(self): + return SSHPublicKey.objects.by_owner_or_group(self.request.user) + + +router.register('sshpublickeys', SSHPublicKeyViewSet) + +# for view in map(__module__.__dict__.get, __all__): +# LOGGER.debug('registering view: %s', view) +# router.register(view.model._meta.verbose_name_plural.lower(), view) diff --git a/src/api/views/repo.py b/src/api/views/repo.py new file mode 100644 index 0000000..248db67 --- /dev/null +++ b/src/api/views/repo.py @@ -0,0 +1,101 @@ +import logging + +from django.db.models import Q +from rest_framework.decorators import action +from rest_framework.response import Response + +from api.lib.viewsets import SimpleHyperlinkedModelViewSet +from api.router import router +from api.serializers import RepositorySerializer, RepositoryEventSerializer, RepositoryStatisticSerializer +from borghive.models import Repository, RepositoryLocation, RepositoryUser, RepositoryEvent, RepositoryStatistic + +LOGGER = logging.getLogger(__name__) + +__all__ = ['RepositoryViewSet'] + + +class RepositoryViewSet(SimpleHyperlinkedModelViewSet): + """ + repository viewset + """ + + # pylint: disable=unused-argument + + queryset = Repository.objects.all() + serializer_class = RepositorySerializer + model = Repository + + def get_queryset(self): + return Repository.objects.by_owner_or_group(self.request.user) + + @action(methods=['get'], detail=True) + def events(self, request, pk=None): + """ + detail view on repository events + """ + events = self.get_object().repositoryevent_set.all() + serializer = RepositoryEventSerializer(events, many=True, context={'request': request}) + return Response(serializer.data) + + @action(methods=['get'], detail=True) + def statistics(self, request, pk=None): + """ + detail view on repository statistics + """ + stats = self.get_object().repositorystatistic_set.all() + serializer = RepositoryStatisticSerializer(stats, many=True, context={'request': request}) + return Response(serializer.data) + + +class RepositoryUserViewSet(SimpleHyperlinkedModelViewSet): + """ + repositoryuser viewset + """ + queryset = RepositoryUser.objects.all() + model = RepositoryUser + + def get_queryset(self): + return RepositoryUser.objects.filter(Q(repository__owner=self.request.user) | Q(repository__group__in=self.request.user.groups.all())) + + +class RepositoryEventViewSet(SimpleHyperlinkedModelViewSet): + """ + repositoryevent viewset + """ + queryset = RepositoryEvent.objects.all() + serializer_class = RepositoryEventSerializer + model = RepositoryEvent + + def get_queryset(self): + return RepositoryEvent.objects.filter(Q(repo__owner=self.request.user) | Q(repo__group__in=self.request.user.groups.all())) + + +class RepositoryStatisticViewSet(SimpleHyperlinkedModelViewSet): + """ + repositorystatistic viewset + """ + queryset = RepositoryStatistic.objects.all() + serializer_class = RepositoryStatisticSerializer + model = RepositoryStatistic + + def get_queryset(self): + return RepositoryStatistic.objects.filter(Q(repo__owner=self.request.user) | Q(repo__group__in=self.request.user.groups.all())) + + +class RepositoryLocationViewSet(SimpleHyperlinkedModelViewSet): + """ + repositorylocation viewset + """ + queryset = RepositoryLocation.objects.all() + model = RepositoryLocation + + +router.register('repositories', RepositoryViewSet) +router.register('repository-users', RepositoryUserViewSet) +router.register('repository-events', RepositoryEventViewSet) +router.register('repository-statistics', RepositoryStatisticViewSet) +router.register('locations', RepositoryLocationViewSet) + +# for view in map(__module__.__dict__.get, __all__): +# LOGGER.debug('registering view: %s', view) +# router.register(view.model._meta.verbose_name_plural.lower(), view) diff --git a/src/api/views/user.py b/src/api/views/user.py new file mode 100644 index 0000000..155acc5 --- /dev/null +++ b/src/api/views/user.py @@ -0,0 +1,29 @@ +from django.contrib.auth.models import Group, User + +from api.lib.viewsets import SimpleHyperlinkedModelViewSet +# from api.router import router +from api.serializers import SimpleGroupSerializer, SimpleOwnerSerializer + +__all__ = ['UserViewset', 'GroupViewset'] + + +class UserViewset(SimpleHyperlinkedModelViewSet): + """ + user viewset for api + """ + queryset = User.objects.all() + serializer_class = SimpleOwnerSerializer + model = User + + +class GroupViewset(SimpleHyperlinkedModelViewSet): + """ + group viewset for api + """ + queryset = Group.objects.all() + serializer_class = SimpleGroupSerializer + model = Group + + +# router.register('users', UserViewset) +# router.register('groups', GroupViewset) diff --git a/src/borghive/lib/validators.py b/src/borghive/lib/validators.py index 672b1e1..352f512 100644 --- a/src/borghive/lib/validators.py +++ b/src/borghive/lib/validators.py @@ -11,8 +11,9 @@ def ssh_public_key_validator(public_key): validate public key string ''' try: + key = sshpubkeys.SSHKey(public_key) key.parse() except (sshpubkeys.InvalidKeyError, sshpubkeys.exceptions.MalformedDataError, UnicodeEncodeError) as exc: LOGGER.exception(exc) - raise ValidationError('Malformed SSH Public Key') + raise ValidationError('Malformed SSH Public Key') from exc diff --git a/src/borghive/managers.py b/src/borghive/managers.py new file mode 100644 index 0000000..6001b24 --- /dev/null +++ b/src/borghive/managers.py @@ -0,0 +1,16 @@ +from django.db import models +from django.db.models import Q + + +class OwnerOrGroupManager(models.Manager): + """ + model manager for filtering by owner or group + """ + + # pylint: disable=R0903 + + def by_owner_or_group(self, user): + """ + use Django Q to filter by owner or group of user + """ + return self.get_queryset().filter(Q(owner=user) | Q(group__in=user.groups.all())) diff --git a/src/borghive/migrations/0002_append_only.py b/src/borghive/migrations/0002_append_only.py index 0ce7697..2437685 100644 --- a/src/borghive/migrations/0002_append_only.py +++ b/src/borghive/migrations/0002_append_only.py @@ -32,7 +32,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='repository', name='append_only_keys', - field=models.ManyToManyField(related_name='append_only_keys', to='borghive.SSHPublicKey'), + field=models.ManyToManyField(blank=True, related_name='append_only_keys', to='borghive.SSHPublicKey'), ), migrations.AlterField( model_name='repository', diff --git a/src/borghive/migrations/0004_stats_unit.py b/src/borghive/migrations/0004_stats_unit.py new file mode 100644 index 0000000..89cdcba --- /dev/null +++ b/src/borghive/migrations/0004_stats_unit.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-09-19 20:15 + +from django.db import migrations, models +import ldapdb.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('borghive', '0003_repo_mode'), + ] + + operations = [ + migrations.AddField( + model_name='repositorystatistic', + name='repo_size_unit', + field=models.CharField(default='MB', max_length=3), + preserve_default=False, + ) + ] diff --git a/src/borghive/models/base.py b/src/borghive/models/base.py index 20d20af..3a79d39 100644 --- a/src/borghive/models/base.py +++ b/src/borghive/models/base.py @@ -1,4 +1,5 @@ from django.db import models + from rules.contrib.models import RulesModelBase, RulesModelMixin diff --git a/src/borghive/models/key.py b/src/borghive/models/key.py index 8fcafd3..3c0bad2 100644 --- a/src/borghive/models/key.py +++ b/src/borghive/models/key.py @@ -8,6 +8,7 @@ from borghive.lib.validators import ssh_public_key_validator from borghive.models.base import BaseModel +from borghive.managers import OwnerOrGroupManager LOGGER = logging.getLogger(__name__) @@ -36,6 +37,8 @@ class SSHPublicKey(BaseModel): owner = models.ForeignKey(User, on_delete=models.PROTECT) group = models.ManyToManyField(Group, blank=True) + objects = OwnerOrGroupManager() + def __str__(self): """representation""" return 'SSHPublicKey: {}'.format(self.name) diff --git a/src/borghive/models/repository.py b/src/borghive/models/repository.py index f9d323f..44f66da 100644 --- a/src/borghive/models/repository.py +++ b/src/borghive/models/repository.py @@ -18,6 +18,7 @@ from borghive.lib.user import generate_userid from borghive.models.base import BaseModel from borghive.models.ldap import RepositoryLdapUser +from borghive.managers import OwnerOrGroupManager from .key import SSHPublicKey @@ -146,6 +147,8 @@ class Repository(BaseModel): alert_after_days = models.IntegerField(null=True, blank=True) # days + objects = OwnerOrGroupManager() + def __str__(self): """representation""" return 'Repository: {}'.format(self.name) @@ -231,7 +234,7 @@ def refresh(self): self.save() # create statistic - statistic = RepositoryStatistic(repo_size=self.get_repo_size()) + statistic = RepositoryStatistic(repo_size=self.get_repo_size(), repo_size_unit="MB") statistic.repo = self statistic.save() else: @@ -328,6 +331,7 @@ class RepositoryStatistic(BaseModel): repository statistic """ repo_size = models.IntegerField() # mega bytes + repo_size_unit = models.CharField(max_length=3) repo = models.ForeignKey(Repository, on_delete=models.CASCADE) def __str__(self): diff --git a/src/borghive/tests/test_key.py b/src/borghive/tests/test_key.py index 05c5df1..1baddd1 100644 --- a/src/borghive/tests/test_key.py +++ b/src/borghive/tests/test_key.py @@ -87,7 +87,7 @@ def test_create_valid_key_newline(self): response = self.client.post(reverse('key-create'), data=data) self.assertEqual(SSHPublicKey.objects.count(), 1) - + def test_create_invalid_key_without_comment(self): data = { 'name': 'key-ed25519-x', @@ -128,7 +128,7 @@ def test_invalid_key3(self): response = self.client.post(reverse('key-create'), data=data) self.assertEqual(SSHPublicKey.objects.count(), 0) - def test_invalid_key3(self): + def test_invalid_key4(self): data = { 'name': 'key-asdf', 'public_key': 'ssh-rsa AAAAC3xölsdflkdf$$$$$@@@$$adx' diff --git a/src/borghive/views/repository.py b/src/borghive/views/repository.py index 18c4314..6736199 100644 --- a/src/borghive/views/repository.py +++ b/src/borghive/views/repository.py @@ -26,7 +26,7 @@ class RepositoryListView(BaseView, ListView): def get_total_usage(self): # pylint: disable=no-self-use """get total usage from repostatistic of all repos""" total_size = 0 - for repo in Repository.objects.all(): + for repo in Repository.objects.by_owner_or_group(user=self.request.user): stat = repo.get_last_repository_statistic() if stat: total_size += stat.repo_size diff --git a/src/core/settings.py b/src/core/settings.py index 2de11c4..dabbfcf 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -50,8 +50,10 @@ 'django_extensions', 'django_celery_beat', 'crispy_forms', + 'rest_framework', 'rules', - 'borghive', + 'api', + 'borghive' ] MIDDLEWARE = [ @@ -194,6 +196,11 @@ 'propagate': True, 'level': env('APP_LOG_LEVEL', 'DEBUG') }, + 'api': { + 'handlers': ['console'], + 'propagate': True, + 'level': env('APP_LOG_LEVEL', 'DEBUG') + }, 'rules': { 'handlers': ['console'], 'propagate': True, @@ -217,7 +224,7 @@ BORGHIVE = { 'CONFIG_PATH': env('CONFIG_PATH', '/config'), 'REPO_PATH': env('BORGHIVE_REPO_PATH', '/repos'), - 'SSH_PUBLIC_KEY_REGEX': r'^((ssh|ecdsa)-[a-zA-Z0-9-]+) (AAAA[0-9A-Za-z+/=]+)( [\w\-@]+)?$', + 'SSH_PUBLIC_KEY_REGEX': r'^((ssh|ecdsa)-[a-zA-Z0-9-]+) (AAAA[0-9A-Za-z+/=]+)( [\w\-@_\.]+)?$', 'LDAP_USER_BASEDN': env('BORGHIVE_LDAP_USER_BASEDN', 'dc=borghive,dc=local') } @@ -232,3 +239,10 @@ EMAIL_FROM = env('EMAIL_FROM', 'borghive@{}'.format(socket.getfqdn())) if DEBUG: EMAIL_BACKEND = env('EMAIL_BACKEND', 'django.core.mail.backends.dummy.EmailBackend') + +# +# DJANGO REST FRAMEWORK SETTINGS +# +REST_FRAMEWORK = { + 'URL_FIELD_NAME': '_href' +} diff --git a/src/core/urls.py b/src/core/urls.py index 81cbd33..e948556 100644 --- a/src/core/urls.py +++ b/src/core/urls.py @@ -19,6 +19,7 @@ urlpatterns = [ path('', include('borghive.urls')), + path('api/', include('api.urls')), path('accounts/', include('django.contrib.auth.urls')), path('admin/', admin.site.urls) ]