From 47103df48a7c9de61f359bdbd0bba4235a1f92be Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sat, 7 Nov 2020 18:58:29 +0200 Subject: [PATCH 1/7] Change user creation to not ask for a password (and clarify the readme). --- README.md | 7 +++---- myauth/admin.py | 13 ++++++++++++- myauth/forms.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 myauth/forms.py diff --git a/README.md b/README.md index 0ecf52a..3a3f5fd 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,9 @@ Create yourself an admin user: ``` At this stage you need to create accounts to be used with the EteSync apps. To do that, please go to: -`www.your-etesync-install.com/admin` and create a new user to be used with the service. Set a random -password for the user such as `j3PmCRftyQMtM3eWvi8f`. No need to remember it, as it won't be used. -Etebase uses a zero-knowledge proof for authentication, so the user will just create a password when -creating the account from the apps. +`www.your-etesync-install.com/admin` and create a new user to be used with the service. No need to set +a password, as Etebase uses a zero-knowledge proof for authentication, so the user will just create +a password when creating the account from the apps. After this user has been created, you can use any of the EteSync apps to signup (or login) with the same username and email in order to set up the account. The password used at that point will be used to setup the account. diff --git a/myauth/admin.py b/myauth/admin.py index f91be8f..0ecde3f 100644 --- a/myauth/admin.py +++ b/myauth/admin.py @@ -1,5 +1,16 @@ from django.contrib import admin -from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin from .models import User +from .forms import AdminUserCreationForm + + +class UserAdmin(DjangoUserAdmin): + add_form = AdminUserCreationForm + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('username', ), + }), + ) admin.site.register(User, UserAdmin) diff --git a/myauth/forms.py b/myauth/forms.py new file mode 100644 index 0000000..55f7299 --- /dev/null +++ b/myauth/forms.py @@ -0,0 +1,30 @@ +from django import forms +from django.contrib.auth import get_user_model +from django.contrib.auth.forms import UsernameField + +User = get_user_model() + + +class AdminUserCreationForm(forms.ModelForm): + """ + A form that creates a user, with no privileges, from the given username and + password. + """ + + class Meta: + model = User + fields = ("username",) + field_classes = {'username': UsernameField} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self._meta.model.USERNAME_FIELD in self.fields: + self.fields[self._meta.model.USERNAME_FIELD].widget.attrs['autofocus'] = True + + def save(self, commit=True): + user = super().save(commit=False) + user.set_unusable_password() + if commit: + user.save() + return user + From e9de8f1adb16b47becf700100333c38b8e860f5c Mon Sep 17 00:00:00 2001 From: "Victor R. Santos" Date: Sat, 7 Nov 2020 16:21:34 -0300 Subject: [PATCH 2/7] Add env variable to change configuration file path. ETEBASE_EASY_CONFIG_PATH is optional, the server serches for the configurations files in this order: - "ETEBASE_EASY_CONFIG_PATH" - etebase-server.ini - /etc/etebase-server/etebase-server.ini --- etebase_server/settings.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/etebase_server/settings.py b/etebase_server/settings.py index f785cb7..ee98f55 100644 --- a/etebase_server/settings.py +++ b/etebase_server/settings.py @@ -141,7 +141,12 @@ MEDIA_URL = '/user-media/' # Define where to find configuration files -config_locations = ['etebase-server.ini', '/etc/etebase-server/etebase-server.ini'] +config_locations = [ + os.environ.get('ETEBASE_EASY_CONFIG_PATH', ''), + 'etebase-server.ini', + '/etc/etebase-server/etebase-server.ini', +] + # Use config file if present if any(os.path.isfile(x) for x in config_locations): config = configparser.ConfigParser() From bdd787b9158cee532fc9ba1e579d7ef7337dcf8a Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 9 Nov 2020 17:31:12 +0200 Subject: [PATCH 3/7] Gracefully handle uploading the same item twice. We were failing until now, but since the uid is sure to be unique, we can just assume that if it's the same uid it's the same content. This means we can just gracefully fail as the data is the same. Until now, we were raising an error, but we now just do nothing and consider it a success. This is especially useful when a network error caused an item to be uploaded but not updated on the client side. --- django_etebase/serializers.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/django_etebase/serializers.py b/django_etebase/serializers.py index 97dcd64..4f2d802 100644 --- a/django_etebase/serializers.py +++ b/django_etebase/serializers.py @@ -30,6 +30,10 @@ User = get_user_model() def process_revisions_for_item(item, revision_data): chunks_objs = [] chunks = revision_data.pop('chunks_relation') + + revision = models.CollectionItemRevision(**revision_data, item=item) + revision.validate_unique() # Verify there aren't any validation issues + for chunk in chunks: uid = chunk[0] chunk_obj = models.CollectionItemChunk.objects.filter(uid=uid).first() @@ -47,8 +51,9 @@ def process_revisions_for_item(item, revision_data): chunks_objs.append(chunk_obj) stoken = models.Stoken.objects.create() + revision.stoken = stoken + revision.save() - revision = models.CollectionItemRevision.objects.create(**revision_data, item=item, stoken=stoken) for chunk in chunks_objs: models.RevisionChunkRelation.objects.create(chunk=chunk, revision=revision) return revision @@ -196,6 +201,9 @@ class CollectionItemRevisionSerializer(BetterErrorsMixin, serializers.ModelSeria class Meta: model = models.CollectionItemRevision fields = ('chunks', 'meta', 'uid', 'deleted') + extra_kwargs = { + 'uid': {'validators': []}, # We deal with it in the serializers + } class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer): @@ -220,6 +228,10 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer): instance, created = Model.objects.get_or_create(uid=uid, defaults=validated_data) cur_etag = instance.etag if not created else None + # If we are trying to update an up to date item, abort early and consider it a success + if cur_etag == revision_data.get('uid'): + return instance + if validate_etag and cur_etag != etag: raise EtebaseValidationError('wrong_etag', 'Wrong etag. Expected {} got {}'.format(cur_etag, etag), status_code=status.HTTP_409_CONFLICT) @@ -231,7 +243,10 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer): current_revision.current = None current_revision.save() - process_revisions_for_item(instance, revision_data) + try: + process_revisions_for_item(instance, revision_data) + except django_exceptions.ValidationError as e: + self.transform_validation_error("content", e) return instance From ab8b2bc58aba87e40d8df91af83dd4e9c1519b62 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 12 Nov 2020 14:07:27 +0200 Subject: [PATCH 4/7] README: update + add chat badge. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a3f5fd..ebb384e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@

Etebase - Encrypt Everything

-A skeleton app for running your own [Etebase](https://www.etebase.com) (EteSync 2.0) server. +An [Etebase](https://www.etebase.com) (EteSync 2.0) server so you can run your own. + +[![Chat with us](https://img.shields.io/badge/chat-IRC%20|%20Matrix%20|%20Web-blue.svg)](https://www.etebase.com/community-chat/) # Installation From 9ec16e921623acc80a405aaf96306e0e9ece83fe Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Sat, 14 Nov 2020 16:56:26 +0200 Subject: [PATCH 5/7] Update changelog. --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 8845089..d3adef0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,10 @@ # Changelog +## Version 0.5.2 +* Fix issues with host verification failing with a custom port +* Add env variable to change configuration file path. +* Change user creation to not ask for a password (and clarify the readme). + ## Version 0.5.1 * Enforce collections to always have a collection type set * Collection saving: add another verification for collection UID uniqueness. From d8e5c37db1b5876ca0664721647b7c14bf4d8a9e Mon Sep 17 00:00:00 2001 From: Tal Leibman <42600279+Tal-Leibman@users.noreply.github.com> Date: Sat, 14 Nov 2020 17:04:41 +0200 Subject: [PATCH 6/7] Use black for code formatting and format the code Merge #65 --- django_etebase/app_settings.py | 16 +- django_etebase/apps.py | 2 +- django_etebase/drf_msgpack/apps.py | 2 +- django_etebase/drf_msgpack/parsers.py | 4 +- django_etebase/drf_msgpack/renderers.py | 8 +- django_etebase/exceptions.py | 7 +- django_etebase/migrations/0001_initial.py | 180 +++++--- django_etebase/migrations/0002_userinfo.py | 22 +- .../migrations/0003_collectioninvitation.py | 50 ++- .../0004_collectioninvitation_version.py | 6 +- .../migrations/0005_auto_20200526_1021.py | 8 +- .../migrations/0006_auto_20200526_1040.py | 14 +- .../migrations/0007_auto_20200526_1336.py | 66 ++- .../migrations/0008_auto_20200526_1535.py | 29 +- .../migrations/0009_auto_20200526_1535.py | 6 +- .../migrations/0010_auto_20200526_1539.py | 8 +- .../0011_collectionmember_stoken.py | 10 +- .../migrations/0012_auto_20200527_0743.py | 6 +- .../0013_collectionmemberremoved.py | 28 +- .../migrations/0014_auto_20200602_1558.py | 8 +- .../0015_collectionitemrevision_salt.py | 6 +- .../migrations/0016_auto_20200623_0820.py | 28 +- .../migrations/0017_auto_20200623_0958.py | 23 +- .../migrations/0018_auto_20200624_0748.py | 14 +- .../migrations/0019_auto_20200626_0748.py | 14 +- ...0020_remove_collectionitemrevision_salt.py | 7 +- .../migrations/0021_auto_20200626_0913.py | 65 ++- .../migrations/0022_auto_20200804_1059.py | 7 +- .../0023_collectionitemchunk_collection.py | 13 +- .../migrations/0024_auto_20200804_1209.py | 4 +- .../migrations/0025_auto_20200804_1216.py | 20 +- .../migrations/0026_auto_20200907_0752.py | 14 +- .../migrations/0027_auto_20200907_0752.py | 14 +- .../migrations/0028_auto_20200907_0754.py | 18 +- .../migrations/0029_auto_20200907_0801.py | 12 +- .../migrations/0030_auto_20200922_0832.py | 13 +- .../migrations/0031_auto_20201013_1336.py | 18 +- .../migrations/0032_auto_20201013_1409.py | 6 +- django_etebase/models.py | 114 ++--- django_etebase/parsers.py | 7 +- django_etebase/permissions.py | 21 +- django_etebase/renderers.py | 1 + django_etebase/serializers.py | 237 ++++++----- django_etebase/signals.py | 2 +- django_etebase/token_auth/apps.py | 2 +- django_etebase/token_auth/authentication.py | 10 +- .../token_auth/migrations/0001_initial.py | 22 +- django_etebase/token_auth/models.py | 5 +- django_etebase/urls.py | 28 +- django_etebase/utils.py | 4 +- django_etebase/views.py | 395 +++++++++--------- etebase_server/asgi.py | 2 +- etebase_server/settings.py | 143 +++---- etebase_server/urls.py | 9 +- etebase_server/utils.py | 1 + etebase_server/wsgi.py | 2 +- manage.py | 4 +- myauth/admin.py | 8 +- myauth/apps.py | 2 +- myauth/forms.py | 5 +- myauth/migrations/0001_initial.py | 91 +++- myauth/migrations/0002_auto_20200515_0801.py | 15 +- myauth/models.py | 17 +- pyproject.toml | 2 + requirements.in/development.txt | 1 + 65 files changed, 1094 insertions(+), 832 deletions(-) create mode 100644 pyproject.toml diff --git a/django_etebase/app_settings.py b/django_etebase/app_settings.py index 3c580b2..7c93f5f 100644 --- a/django_etebase/app_settings.py +++ b/django_etebase/app_settings.py @@ -21,18 +21,19 @@ class AppSettings: def import_from_str(self, name): from importlib import import_module - path, prop = name.rsplit('.', 1) + path, prop = name.rsplit(".", 1) mod = import_module(path) return getattr(mod, prop) def _setting(self, name, dflt): from django.conf import settings + return getattr(settings, self.prefix + name, dflt) @cached_property def API_PERMISSIONS(self): # pylint: disable=invalid-name - perms = self._setting("API_PERMISSIONS", ('rest_framework.permissions.IsAuthenticated', )) + perms = self._setting("API_PERMISSIONS", ("rest_framework.permissions.IsAuthenticated",)) ret = [] for perm in perms: ret.append(self.import_from_str(perm)) @@ -40,8 +41,13 @@ class AppSettings: @cached_property def API_AUTHENTICATORS(self): # pylint: disable=invalid-name - perms = self._setting("API_AUTHENTICATORS", ('rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication')) + perms = self._setting( + "API_AUTHENTICATORS", + ( + "rest_framework.authentication.TokenAuthentication", + "rest_framework.authentication.SessionAuthentication", + ), + ) ret = [] for perm in perms: ret.append(self.import_from_str(perm)) @@ -80,4 +86,4 @@ class AppSettings: return self._setting("CHALLENGE_VALID_SECONDS", 60) -app_settings = AppSettings('ETEBASE_') +app_settings = AppSettings("ETEBASE_") diff --git a/django_etebase/apps.py b/django_etebase/apps.py index 286a708..84e4b6e 100644 --- a/django_etebase/apps.py +++ b/django_etebase/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class DjangoEtebaseConfig(AppConfig): - name = 'django_etebase' + name = "django_etebase" diff --git a/django_etebase/drf_msgpack/apps.py b/django_etebase/drf_msgpack/apps.py index 619e3e0..22ea2c1 100644 --- a/django_etebase/drf_msgpack/apps.py +++ b/django_etebase/drf_msgpack/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class DrfMsgpackConfig(AppConfig): - name = 'drf_msgpack' + name = "drf_msgpack" diff --git a/django_etebase/drf_msgpack/parsers.py b/django_etebase/drf_msgpack/parsers.py index 44cd33b..0504a76 100644 --- a/django_etebase/drf_msgpack/parsers.py +++ b/django_etebase/drf_msgpack/parsers.py @@ -5,10 +5,10 @@ from rest_framework.exceptions import ParseError class MessagePackParser(BaseParser): - media_type = 'application/msgpack' + media_type = "application/msgpack" def parse(self, stream, media_type=None, parser_context=None): try: return msgpack.unpackb(stream.read(), raw=False) except Exception as exc: - raise ParseError('MessagePack parse error - %s' % str(exc)) + raise ParseError("MessagePack parse error - %s" % str(exc)) diff --git a/django_etebase/drf_msgpack/renderers.py b/django_etebase/drf_msgpack/renderers.py index 9445231..35a4afa 100644 --- a/django_etebase/drf_msgpack/renderers.py +++ b/django_etebase/drf_msgpack/renderers.py @@ -4,12 +4,12 @@ from rest_framework.renderers import BaseRenderer class MessagePackRenderer(BaseRenderer): - media_type = 'application/msgpack' - format = 'msgpack' - render_style = 'binary' + media_type = "application/msgpack" + format = "msgpack" + render_style = "binary" charset = None def render(self, data, media_type=None, renderer_context=None): if data is None: - return b'' + return b"" return msgpack.packb(data, use_bin_type=True) diff --git a/django_etebase/exceptions.py b/django_etebase/exceptions.py index d05c4e5..f3aa08a 100644 --- a/django_etebase/exceptions.py +++ b/django_etebase/exceptions.py @@ -3,8 +3,7 @@ from rest_framework import serializers, status class EtebaseValidationError(serializers.ValidationError): def __init__(self, code, detail, status_code=status.HTTP_400_BAD_REQUEST): - super().__init__({ - 'code': code, - 'detail': detail, - }) + super().__init__( + {"code": code, "detail": detail,} + ) self.status_code = status_code diff --git a/django_etebase/migrations/0001_initial.py b/django_etebase/migrations/0001_initial.py index 69a9a91..86f0fa6 100644 --- a/django_etebase/migrations/0001_initial.py +++ b/django_etebase/migrations/0001_initial.py @@ -17,75 +17,159 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Collection', + name="Collection", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uid', models.CharField(db_index=True, max_length=44, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='[a-zA-Z0-9]')])), - ('version', models.PositiveSmallIntegerField()), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "uid", + models.CharField( + db_index=True, + max_length=44, + validators=[ + django.core.validators.RegexValidator(message="Not a valid UID", regex="[a-zA-Z0-9]") + ], + ), + ), + ("version", models.PositiveSmallIntegerField()), + ("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], - options={ - 'unique_together': {('uid', 'owner')}, - }, + options={"unique_together": {("uid", "owner")},}, ), migrations.CreateModel( - name='CollectionItem', + name="CollectionItem", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uid', models.CharField(db_index=True, max_length=44, null=True, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='[a-zA-Z0-9]')])), - ('version', models.PositiveSmallIntegerField()), - ('encryptionKey', models.BinaryField(editable=True, null=True)), - ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='django_etebase.Collection')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "uid", + models.CharField( + db_index=True, + max_length=44, + null=True, + validators=[ + django.core.validators.RegexValidator(message="Not a valid UID", regex="[a-zA-Z0-9]") + ], + ), + ), + ("version", models.PositiveSmallIntegerField()), + ("encryptionKey", models.BinaryField(editable=True, null=True)), + ( + "collection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="items", + to="django_etebase.Collection", + ), + ), ], - options={ - 'unique_together': {('uid', 'collection')}, - }, + options={"unique_together": {("uid", "collection")},}, ), migrations.CreateModel( - name='CollectionItemChunk', + name="CollectionItemChunk", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uid', models.CharField(db_index=True, max_length=44, validators=[django.core.validators.RegexValidator(message='Expected a 256bit base64url.', regex='^[a-zA-Z0-9\\-_]{43}$')])), - ('chunkFile', models.FileField(max_length=150, unique=True, upload_to=django_etebase.models.chunk_directory_path)), - ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='django_etebase.CollectionItem')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "uid", + models.CharField( + db_index=True, + max_length=44, + validators=[ + django.core.validators.RegexValidator( + message="Expected a 256bit base64url.", regex="^[a-zA-Z0-9\\-_]{43}$" + ) + ], + ), + ), + ( + "chunkFile", + models.FileField(max_length=150, unique=True, upload_to=django_etebase.models.chunk_directory_path), + ), + ( + "item", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="chunks", + to="django_etebase.CollectionItem", + ), + ), ], ), migrations.CreateModel( - name='CollectionItemRevision', + name="CollectionItemRevision", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uid', models.CharField(db_index=True, max_length=44, unique=True, validators=[django.core.validators.RegexValidator(message='Expected a 256bit base64url.', regex='^[a-zA-Z0-9\\-_]{43}$')])), - ('meta', models.BinaryField(editable=True)), - ('current', models.BooleanField(db_index=True, default=True, null=True)), - ('deleted', models.BooleanField(default=False)), - ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='django_etebase.CollectionItem')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "uid", + models.CharField( + db_index=True, + max_length=44, + unique=True, + validators=[ + django.core.validators.RegexValidator( + message="Expected a 256bit base64url.", regex="^[a-zA-Z0-9\\-_]{43}$" + ) + ], + ), + ), + ("meta", models.BinaryField(editable=True)), + ("current", models.BooleanField(db_index=True, default=True, null=True)), + ("deleted", models.BooleanField(default=False)), + ( + "item", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="revisions", + to="django_etebase.CollectionItem", + ), + ), ], - options={ - 'unique_together': {('item', 'current')}, - }, + options={"unique_together": {("item", "current")},}, ), migrations.CreateModel( - name='RevisionChunkRelation', + name="RevisionChunkRelation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('chunk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions_relation', to='django_etebase.CollectionItemChunk')), - ('revision', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chunks_relation', to='django_etebase.CollectionItemRevision')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "chunk", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="revisions_relation", + to="django_etebase.CollectionItemChunk", + ), + ), + ( + "revision", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="chunks_relation", + to="django_etebase.CollectionItemRevision", + ), + ), ], - options={ - 'ordering': ('id',), - }, + options={"ordering": ("id",),}, ), migrations.CreateModel( - name='CollectionMember', + name="CollectionMember", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('encryptionKey', models.BinaryField(editable=True)), - ('accessLevel', models.CharField(choices=[('adm', 'Admin'), ('rw', 'Read Write'), ('ro', 'Read Only')], default='ro', max_length=3)), - ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='django_etebase.Collection')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("encryptionKey", models.BinaryField(editable=True)), + ( + "accessLevel", + models.CharField( + choices=[("adm", "Admin"), ("rw", "Read Write"), ("ro", "Read Only")], + default="ro", + max_length=3, + ), + ), + ( + "collection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="members", + to="django_etebase.Collection", + ), + ), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], - options={ - 'unique_together': {('user', 'collection')}, - }, + options={"unique_together": {("user", "collection")},}, ), ] diff --git a/django_etebase/migrations/0002_userinfo.py b/django_etebase/migrations/0002_userinfo.py index 6da0bb8..6ddd9a5 100644 --- a/django_etebase/migrations/0002_userinfo.py +++ b/django_etebase/migrations/0002_userinfo.py @@ -8,18 +8,26 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('myauth', '0001_initial'), - ('django_etebase', '0001_initial'), + ("myauth", "0001_initial"), + ("django_etebase", "0001_initial"), ] operations = [ migrations.CreateModel( - name='UserInfo', + name="UserInfo", fields=[ - ('owner', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), - ('version', models.PositiveSmallIntegerField(default=1)), - ('pubkey', models.BinaryField(editable=True)), - ('salt', models.BinaryField(editable=True)), + ( + "owner", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to=settings.AUTH_USER_MODEL, + ), + ), + ("version", models.PositiveSmallIntegerField(default=1)), + ("pubkey", models.BinaryField(editable=True)), + ("salt", models.BinaryField(editable=True)), ], ), ] diff --git a/django_etebase/migrations/0003_collectioninvitation.py b/django_etebase/migrations/0003_collectioninvitation.py index 8fd2066..1b416ab 100644 --- a/django_etebase/migrations/0003_collectioninvitation.py +++ b/django_etebase/migrations/0003_collectioninvitation.py @@ -10,22 +10,50 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('django_etebase', '0002_userinfo'), + ("django_etebase", "0002_userinfo"), ] operations = [ migrations.CreateModel( - name='CollectionInvitation', + name="CollectionInvitation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uid', models.CharField(db_index=True, max_length=44, validators=[django.core.validators.RegexValidator(message='Expected a 256bit base64url.', regex='^[a-zA-Z0-9\\-_]{43}$')])), - ('signedEncryptionKey', models.BinaryField()), - ('accessLevel', models.CharField(choices=[('adm', 'Admin'), ('rw', 'Read Write'), ('ro', 'Read Only')], default='ro', max_length=3)), - ('fromMember', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_etebase.CollectionMember')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incoming_invitations', to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "uid", + models.CharField( + db_index=True, + max_length=44, + validators=[ + django.core.validators.RegexValidator( + message="Expected a 256bit base64url.", regex="^[a-zA-Z0-9\\-_]{43}$" + ) + ], + ), + ), + ("signedEncryptionKey", models.BinaryField()), + ( + "accessLevel", + models.CharField( + choices=[("adm", "Admin"), ("rw", "Read Write"), ("ro", "Read Only")], + default="ro", + max_length=3, + ), + ), + ( + "fromMember", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="django_etebase.CollectionMember" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="incoming_invitations", + to=settings.AUTH_USER_MODEL, + ), + ), ], - options={ - 'unique_together': {('user', 'fromMember')}, - }, + options={"unique_together": {("user", "fromMember")},}, ), ] diff --git a/django_etebase/migrations/0004_collectioninvitation_version.py b/django_etebase/migrations/0004_collectioninvitation_version.py index 4052116..29ae3f1 100644 --- a/django_etebase/migrations/0004_collectioninvitation_version.py +++ b/django_etebase/migrations/0004_collectioninvitation_version.py @@ -6,13 +6,11 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0003_collectioninvitation'), + ("django_etebase", "0003_collectioninvitation"), ] operations = [ migrations.AddField( - model_name='collectioninvitation', - name='version', - field=models.PositiveSmallIntegerField(default=1), + model_name="collectioninvitation", name="version", field=models.PositiveSmallIntegerField(default=1), ), ] diff --git a/django_etebase/migrations/0005_auto_20200526_1021.py b/django_etebase/migrations/0005_auto_20200526_1021.py index da0dc33..3775277 100644 --- a/django_etebase/migrations/0005_auto_20200526_1021.py +++ b/django_etebase/migrations/0005_auto_20200526_1021.py @@ -6,13 +6,9 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0004_collectioninvitation_version'), + ("django_etebase", "0004_collectioninvitation_version"), ] operations = [ - migrations.RenameField( - model_name='userinfo', - old_name='pubkey', - new_name='loginPubkey', - ), + migrations.RenameField(model_name="userinfo", old_name="pubkey", new_name="loginPubkey",), ] diff --git a/django_etebase/migrations/0006_auto_20200526_1040.py b/django_etebase/migrations/0006_auto_20200526_1040.py index b86a996..07b01cd 100644 --- a/django_etebase/migrations/0006_auto_20200526_1040.py +++ b/django_etebase/migrations/0006_auto_20200526_1040.py @@ -6,20 +6,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0005_auto_20200526_1021'), + ("django_etebase", "0005_auto_20200526_1021"), ] operations = [ migrations.AddField( - model_name='userinfo', - name='encryptedSeckey', - field=models.BinaryField(default=b'', editable=True), + model_name="userinfo", + name="encryptedSeckey", + field=models.BinaryField(default=b"", editable=True), preserve_default=False, ), migrations.AddField( - model_name='userinfo', - name='pubkey', - field=models.BinaryField(default=b'', editable=True), + model_name="userinfo", + name="pubkey", + field=models.BinaryField(default=b"", editable=True), preserve_default=False, ), ] diff --git a/django_etebase/migrations/0007_auto_20200526_1336.py b/django_etebase/migrations/0007_auto_20200526_1336.py index 79978c7..01afe45 100644 --- a/django_etebase/migrations/0007_auto_20200526_1336.py +++ b/django_etebase/migrations/0007_auto_20200526_1336.py @@ -7,33 +7,67 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0006_auto_20200526_1040'), + ("django_etebase", "0006_auto_20200526_1040"), ] operations = [ migrations.AlterField( - model_name='collection', - name='uid', - field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9]*$')]), + model_name="collection", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + validators=[django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9]*$")], + ), ), migrations.AlterField( - model_name='collectioninvitation', - name='uid', - field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Expected a base64url.', regex='^[a-zA-Z0-9\\-_]{42,43}$')]), + model_name="collectioninvitation", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + validators=[ + django.core.validators.RegexValidator( + message="Expected a base64url.", regex="^[a-zA-Z0-9\\-_]{42,43}$" + ) + ], + ), ), migrations.AlterField( - model_name='collectionitem', - name='uid', - field=models.CharField(db_index=True, max_length=43, null=True, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9]*$')]), + model_name="collectionitem", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + null=True, + validators=[django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9]*$")], + ), ), migrations.AlterField( - model_name='collectionitemchunk', - name='uid', - field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Expected a base64url.', regex='^[a-zA-Z0-9\\-_]{42,43}$')]), + model_name="collectionitemchunk", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + validators=[ + django.core.validators.RegexValidator( + message="Expected a base64url.", regex="^[a-zA-Z0-9\\-_]{42,43}$" + ) + ], + ), ), migrations.AlterField( - model_name='collectionitemrevision', - name='uid', - field=models.CharField(db_index=True, max_length=43, unique=True, validators=[django.core.validators.RegexValidator(message='Expected a base64url.', regex='^[a-zA-Z0-9\\-_]{42,43}$')]), + model_name="collectionitemrevision", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + unique=True, + validators=[ + django.core.validators.RegexValidator( + message="Expected a base64url.", regex="^[a-zA-Z0-9\\-_]{42,43}$" + ) + ], + ), ), ] diff --git a/django_etebase/migrations/0008_auto_20200526_1535.py b/django_etebase/migrations/0008_auto_20200526_1535.py index 12656c0..7bb83d5 100644 --- a/django_etebase/migrations/0008_auto_20200526_1535.py +++ b/django_etebase/migrations/0008_auto_20200526_1535.py @@ -9,20 +9,35 @@ import django_etebase.models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0007_auto_20200526_1336'), + ("django_etebase", "0007_auto_20200526_1336"), ] operations = [ migrations.CreateModel( - name='Stoken', + name="Stoken", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uid', models.CharField(db_index=True, default=django_etebase.models.generate_stoken_uid, max_length=43, unique=True, validators=[django.core.validators.RegexValidator(message='Expected a base64url.', regex='^[a-zA-Z0-9\\-_]{42,43}$')])), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "uid", + models.CharField( + db_index=True, + default=django_etebase.models.generate_stoken_uid, + max_length=43, + unique=True, + validators=[ + django.core.validators.RegexValidator( + message="Expected a base64url.", regex="^[a-zA-Z0-9\\-_]{42,43}$" + ) + ], + ), + ), ], ), migrations.AddField( - model_name='collectionitemrevision', - name='stoken', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken'), + model_name="collectionitemrevision", + name="stoken", + field=models.OneToOneField( + null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken" + ), ), ] diff --git a/django_etebase/migrations/0009_auto_20200526_1535.py b/django_etebase/migrations/0009_auto_20200526_1535.py index a6ff498..0ab2f8c 100644 --- a/django_etebase/migrations/0009_auto_20200526_1535.py +++ b/django_etebase/migrations/0009_auto_20200526_1535.py @@ -4,8 +4,8 @@ from django.db import migrations def create_stokens(apps, schema_editor): - Stoken = apps.get_model('django_etebase', 'Stoken') - CollectionItemRevision = apps.get_model('django_etebase', 'CollectionItemRevision') + Stoken = apps.get_model("django_etebase", "Stoken") + CollectionItemRevision = apps.get_model("django_etebase", "CollectionItemRevision") for rev in CollectionItemRevision.objects.all(): rev.stoken = Stoken.objects.create() @@ -15,7 +15,7 @@ def create_stokens(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0008_auto_20200526_1535'), + ("django_etebase", "0008_auto_20200526_1535"), ] operations = [ diff --git a/django_etebase/migrations/0010_auto_20200526_1539.py b/django_etebase/migrations/0010_auto_20200526_1539.py index 7ef0eca..204b97d 100644 --- a/django_etebase/migrations/0010_auto_20200526_1539.py +++ b/django_etebase/migrations/0010_auto_20200526_1539.py @@ -7,13 +7,13 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0009_auto_20200526_1535'), + ("django_etebase", "0009_auto_20200526_1535"), ] operations = [ migrations.AlterField( - model_name='collectionitemrevision', - name='stoken', - field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken'), + model_name="collectionitemrevision", + name="stoken", + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken"), ), ] diff --git a/django_etebase/migrations/0011_collectionmember_stoken.py b/django_etebase/migrations/0011_collectionmember_stoken.py index bafaea7..cbe8d06 100644 --- a/django_etebase/migrations/0011_collectionmember_stoken.py +++ b/django_etebase/migrations/0011_collectionmember_stoken.py @@ -7,13 +7,15 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0010_auto_20200526_1539'), + ("django_etebase", "0010_auto_20200526_1539"), ] operations = [ migrations.AddField( - model_name='collectionmember', - name='stoken', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken'), + model_name="collectionmember", + name="stoken", + field=models.OneToOneField( + null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken" + ), ), ] diff --git a/django_etebase/migrations/0012_auto_20200527_0743.py b/django_etebase/migrations/0012_auto_20200527_0743.py index ab6adbc..1f58f82 100644 --- a/django_etebase/migrations/0012_auto_20200527_0743.py +++ b/django_etebase/migrations/0012_auto_20200527_0743.py @@ -4,8 +4,8 @@ from django.db import migrations def create_stokens(apps, schema_editor): - Stoken = apps.get_model('django_etebase', 'Stoken') - CollectionMember = apps.get_model('django_etebase', 'CollectionMember') + Stoken = apps.get_model("django_etebase", "Stoken") + CollectionMember = apps.get_model("django_etebase", "CollectionMember") for member in CollectionMember.objects.all(): member.stoken = Stoken.objects.create() @@ -15,7 +15,7 @@ def create_stokens(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0011_collectionmember_stoken'), + ("django_etebase", "0011_collectionmember_stoken"), ] operations = [ diff --git a/django_etebase/migrations/0013_collectionmemberremoved.py b/django_etebase/migrations/0013_collectionmemberremoved.py index 2641c03..4481e80 100644 --- a/django_etebase/migrations/0013_collectionmemberremoved.py +++ b/django_etebase/migrations/0013_collectionmemberremoved.py @@ -9,20 +9,30 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('django_etebase', '0012_auto_20200527_0743'), + ("django_etebase", "0012_auto_20200527_0743"), ] operations = [ migrations.CreateModel( - name='CollectionMemberRemoved', + name="CollectionMemberRemoved", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='removed_members', to='django_etebase.Collection')), - ('stoken', models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.Stoken')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "collection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="removed_members", + to="django_etebase.Collection", + ), + ), + ( + "stoken", + models.OneToOneField( + null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.Stoken" + ), + ), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], - options={ - 'unique_together': {('user', 'collection')}, - }, + options={"unique_together": {("user", "collection")},}, ), ] diff --git a/django_etebase/migrations/0014_auto_20200602_1558.py b/django_etebase/migrations/0014_auto_20200602_1558.py index d1a555d..42bed52 100644 --- a/django_etebase/migrations/0014_auto_20200602_1558.py +++ b/django_etebase/migrations/0014_auto_20200602_1558.py @@ -6,13 +6,9 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0013_collectionmemberremoved'), + ("django_etebase", "0013_collectionmemberremoved"), ] operations = [ - migrations.RenameField( - model_name='userinfo', - old_name='encryptedSeckey', - new_name='encryptedContent', - ), + migrations.RenameField(model_name="userinfo", old_name="encryptedSeckey", new_name="encryptedContent",), ] diff --git a/django_etebase/migrations/0015_collectionitemrevision_salt.py b/django_etebase/migrations/0015_collectionitemrevision_salt.py index 7f3dd71..c4dc3e9 100644 --- a/django_etebase/migrations/0015_collectionitemrevision_salt.py +++ b/django_etebase/migrations/0015_collectionitemrevision_salt.py @@ -6,13 +6,11 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0014_auto_20200602_1558'), + ("django_etebase", "0014_auto_20200602_1558"), ] operations = [ migrations.AddField( - model_name='collectionitemrevision', - name='salt', - field=models.BinaryField(default=b'', editable=True), + model_name="collectionitemrevision", name="salt", field=models.BinaryField(default=b"", editable=True), ), ] diff --git a/django_etebase/migrations/0016_auto_20200623_0820.py b/django_etebase/migrations/0016_auto_20200623_0820.py index 2c11157..a273b0d 100644 --- a/django_etebase/migrations/0016_auto_20200623_0820.py +++ b/django_etebase/migrations/0016_auto_20200623_0820.py @@ -7,25 +7,21 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0015_collectionitemrevision_salt'), + ("django_etebase", "0015_collectionitemrevision_salt"), ] operations = [ migrations.AddField( - model_name='collection', - name='main_item', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parent', to='django_etebase.CollectionItem'), - ), - migrations.AlterUniqueTogether( - name='collection', - unique_together=set(), - ), - migrations.RemoveField( - model_name='collection', - name='uid', - ), - migrations.RemoveField( - model_name='collection', - name='version', + model_name="collection", + name="main_item", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="parent", + to="django_etebase.CollectionItem", + ), ), + migrations.AlterUniqueTogether(name="collection", unique_together=set(),), + migrations.RemoveField(model_name="collection", name="uid",), + migrations.RemoveField(model_name="collection", name="version",), ] diff --git a/django_etebase/migrations/0017_auto_20200623_0958.py b/django_etebase/migrations/0017_auto_20200623_0958.py index e244b13..dc599aa 100644 --- a/django_etebase/migrations/0017_auto_20200623_0958.py +++ b/django_etebase/migrations/0017_auto_20200623_0958.py @@ -8,18 +8,27 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0016_auto_20200623_0820'), + ("django_etebase", "0016_auto_20200623_0820"), ] operations = [ migrations.AlterField( - model_name='collection', - name='main_item', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent', to='django_etebase.CollectionItem'), + model_name="collection", + name="main_item", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="parent", + to="django_etebase.CollectionItem", + ), ), migrations.AlterField( - model_name='collectionitem', - name='uid', - field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9]*$')]), + model_name="collectionitem", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + validators=[django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9]*$")], + ), ), ] diff --git a/django_etebase/migrations/0018_auto_20200624_0748.py b/django_etebase/migrations/0018_auto_20200624_0748.py index ec59e0c..d2cdf5a 100644 --- a/django_etebase/migrations/0018_auto_20200624_0748.py +++ b/django_etebase/migrations/0018_auto_20200624_0748.py @@ -7,13 +7,19 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0017_auto_20200623_0958'), + ("django_etebase", "0017_auto_20200623_0958"), ] operations = [ migrations.AlterField( - model_name='collectionitem', - name='uid', - field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]*$')]), + model_name="collectionitem", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + validators=[ + django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]*$") + ], + ), ), ] diff --git a/django_etebase/migrations/0019_auto_20200626_0748.py b/django_etebase/migrations/0019_auto_20200626_0748.py index 991ca50..175e4d0 100644 --- a/django_etebase/migrations/0019_auto_20200626_0748.py +++ b/django_etebase/migrations/0019_auto_20200626_0748.py @@ -7,13 +7,19 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0018_auto_20200624_0748'), + ("django_etebase", "0018_auto_20200624_0748"), ] operations = [ migrations.AlterField( - model_name='collectionitemchunk', - name='uid', - field=models.CharField(db_index=True, max_length=60, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]*$')]), + model_name="collectionitemchunk", + name="uid", + field=models.CharField( + db_index=True, + max_length=60, + validators=[ + django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]*$") + ], + ), ), ] diff --git a/django_etebase/migrations/0020_remove_collectionitemrevision_salt.py b/django_etebase/migrations/0020_remove_collectionitemrevision_salt.py index 2df32bf..21d0337 100644 --- a/django_etebase/migrations/0020_remove_collectionitemrevision_salt.py +++ b/django_etebase/migrations/0020_remove_collectionitemrevision_salt.py @@ -6,12 +6,9 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0019_auto_20200626_0748'), + ("django_etebase", "0019_auto_20200626_0748"), ] operations = [ - migrations.RemoveField( - model_name='collectionitemrevision', - name='salt', - ), + migrations.RemoveField(model_name="collectionitemrevision", name="salt",), ] diff --git a/django_etebase/migrations/0021_auto_20200626_0913.py b/django_etebase/migrations/0021_auto_20200626_0913.py index b890384..3bb6e21 100644 --- a/django_etebase/migrations/0021_auto_20200626_0913.py +++ b/django_etebase/migrations/0021_auto_20200626_0913.py @@ -8,33 +8,66 @@ import django_etebase.models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0020_remove_collectionitemrevision_salt'), + ("django_etebase", "0020_remove_collectionitemrevision_salt"), ] operations = [ migrations.AlterField( - model_name='collectioninvitation', - name='uid', - field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), + model_name="collectioninvitation", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + validators=[ + django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]{20,}$") + ], + ), ), migrations.AlterField( - model_name='collectionitem', - name='uid', - field=models.CharField(db_index=True, max_length=43, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), + model_name="collectionitem", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + validators=[ + django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]{20,}$") + ], + ), ), migrations.AlterField( - model_name='collectionitemchunk', - name='uid', - field=models.CharField(db_index=True, max_length=60, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), + model_name="collectionitemchunk", + name="uid", + field=models.CharField( + db_index=True, + max_length=60, + validators=[ + django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]{20,}$") + ], + ), ), migrations.AlterField( - model_name='collectionitemrevision', - name='uid', - field=models.CharField(db_index=True, max_length=43, unique=True, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), + model_name="collectionitemrevision", + name="uid", + field=models.CharField( + db_index=True, + max_length=43, + unique=True, + validators=[ + django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]{20,}$") + ], + ), ), migrations.AlterField( - model_name='stoken', - name='uid', - field=models.CharField(db_index=True, default=django_etebase.models.generate_stoken_uid, max_length=43, unique=True, validators=[django.core.validators.RegexValidator(message='Not a valid UID', regex='^[a-zA-Z0-9\\-_]{20,}$')]), + model_name="stoken", + name="uid", + field=models.CharField( + db_index=True, + default=django_etebase.models.generate_stoken_uid, + max_length=43, + unique=True, + validators=[ + django.core.validators.RegexValidator(message="Not a valid UID", regex="^[a-zA-Z0-9\\-_]{20,}$") + ], + ), ), ] diff --git a/django_etebase/migrations/0022_auto_20200804_1059.py b/django_etebase/migrations/0022_auto_20200804_1059.py index c47e562..60af33f 100644 --- a/django_etebase/migrations/0022_auto_20200804_1059.py +++ b/django_etebase/migrations/0022_auto_20200804_1059.py @@ -6,12 +6,9 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0021_auto_20200626_0913'), + ("django_etebase", "0021_auto_20200626_0913"), ] operations = [ - migrations.AlterUniqueTogether( - name='collectionitemchunk', - unique_together={('item', 'uid')}, - ), + migrations.AlterUniqueTogether(name="collectionitemchunk", unique_together={("item", "uid")},), ] diff --git a/django_etebase/migrations/0023_collectionitemchunk_collection.py b/django_etebase/migrations/0023_collectionitemchunk_collection.py index b5d6841..314302f 100644 --- a/django_etebase/migrations/0023_collectionitemchunk_collection.py +++ b/django_etebase/migrations/0023_collectionitemchunk_collection.py @@ -7,13 +7,18 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0022_auto_20200804_1059'), + ("django_etebase", "0022_auto_20200804_1059"), ] operations = [ migrations.AddField( - model_name='collectionitemchunk', - name='collection', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='django_etebase.Collection'), + model_name="collectionitemchunk", + name="collection", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="chunks", + to="django_etebase.Collection", + ), ), ] diff --git a/django_etebase/migrations/0024_auto_20200804_1209.py b/django_etebase/migrations/0024_auto_20200804_1209.py index 54c80a3..955a4f9 100644 --- a/django_etebase/migrations/0024_auto_20200804_1209.py +++ b/django_etebase/migrations/0024_auto_20200804_1209.py @@ -4,7 +4,7 @@ from django.db import migrations def change_chunk_to_collections(apps, schema_editor): - CollectionItemChunk = apps.get_model('django_etebase', 'CollectionItemChunk') + CollectionItemChunk = apps.get_model("django_etebase", "CollectionItemChunk") for chunk in CollectionItemChunk.objects.all(): chunk.collection = chunk.item.collection @@ -14,7 +14,7 @@ def change_chunk_to_collections(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0023_collectionitemchunk_collection'), + ("django_etebase", "0023_collectionitemchunk_collection"), ] operations = [ diff --git a/django_etebase/migrations/0025_auto_20200804_1216.py b/django_etebase/migrations/0025_auto_20200804_1216.py index 8849f53..91bf4c8 100644 --- a/django_etebase/migrations/0025_auto_20200804_1216.py +++ b/django_etebase/migrations/0025_auto_20200804_1216.py @@ -7,21 +7,17 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0024_auto_20200804_1209'), + ("django_etebase", "0024_auto_20200804_1209"), ] operations = [ migrations.AlterField( - model_name='collectionitemchunk', - name='collection', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='django_etebase.Collection'), - ), - migrations.AlterUniqueTogether( - name='collectionitemchunk', - unique_together={('collection', 'uid')}, - ), - migrations.RemoveField( - model_name='collectionitemchunk', - name='item', + model_name="collectionitemchunk", + name="collection", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="chunks", to="django_etebase.Collection" + ), ), + migrations.AlterUniqueTogether(name="collectionitemchunk", unique_together={("collection", "uid")},), + migrations.RemoveField(model_name="collectionitemchunk", name="item",), ] diff --git a/django_etebase/migrations/0026_auto_20200907_0752.py b/django_etebase/migrations/0026_auto_20200907_0752.py index 38c0b92..3283654 100644 --- a/django_etebase/migrations/0026_auto_20200907_0752.py +++ b/django_etebase/migrations/0026_auto_20200907_0752.py @@ -6,18 +6,10 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0025_auto_20200804_1216'), + ("django_etebase", "0025_auto_20200804_1216"), ] operations = [ - migrations.RenameField( - model_name='collectioninvitation', - old_name='accessLevel', - new_name='accessLevelOld', - ), - migrations.RenameField( - model_name='collectionmember', - old_name='accessLevel', - new_name='accessLevelOld', - ), + migrations.RenameField(model_name="collectioninvitation", old_name="accessLevel", new_name="accessLevelOld",), + migrations.RenameField(model_name="collectionmember", old_name="accessLevel", new_name="accessLevelOld",), ] diff --git a/django_etebase/migrations/0027_auto_20200907_0752.py b/django_etebase/migrations/0027_auto_20200907_0752.py index d822d3d..21f607f 100644 --- a/django_etebase/migrations/0027_auto_20200907_0752.py +++ b/django_etebase/migrations/0027_auto_20200907_0752.py @@ -6,18 +6,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0026_auto_20200907_0752'), + ("django_etebase", "0026_auto_20200907_0752"), ] operations = [ migrations.AddField( - model_name='collectioninvitation', - name='accessLevel', - field=models.IntegerField(choices=[(0, 'Read Only'), (1, 'Admin'), (2, 'Read Write')], default=0), + model_name="collectioninvitation", + name="accessLevel", + field=models.IntegerField(choices=[(0, "Read Only"), (1, "Admin"), (2, "Read Write")], default=0), ), migrations.AddField( - model_name='collectionmember', - name='accessLevel', - field=models.IntegerField(choices=[(0, 'Read Only'), (1, 'Admin'), (2, 'Read Write')], default=0), + model_name="collectionmember", + name="accessLevel", + field=models.IntegerField(choices=[(0, "Read Only"), (1, "Admin"), (2, "Read Write")], default=0), ), ] diff --git a/django_etebase/migrations/0028_auto_20200907_0754.py b/django_etebase/migrations/0028_auto_20200907_0754.py index cb62e63..24c6246 100644 --- a/django_etebase/migrations/0028_auto_20200907_0754.py +++ b/django_etebase/migrations/0028_auto_20200907_0754.py @@ -6,24 +6,24 @@ from django_etebase.models import AccessLevels def change_access_level_to_int(apps, schema_editor): - CollectionMember = apps.get_model('django_etebase', 'CollectionMember') - CollectionInvitation = apps.get_model('django_etebase', 'CollectionInvitation') + CollectionMember = apps.get_model("django_etebase", "CollectionMember") + CollectionInvitation = apps.get_model("django_etebase", "CollectionInvitation") for member in CollectionMember.objects.all(): - if member.accessLevelOld == 'adm': + if member.accessLevelOld == "adm": member.accessLevel = AccessLevels.ADMIN - elif member.accessLevelOld == 'rw': + elif member.accessLevelOld == "rw": member.accessLevel = AccessLevels.READ_WRITE - elif member.accessLevelOld == 'ro': + elif member.accessLevelOld == "ro": member.accessLevel = AccessLevels.READ_ONLY member.save() for invitation in CollectionInvitation.objects.all(): - if invitation.accessLevelOld == 'adm': + if invitation.accessLevelOld == "adm": invitation.accessLevel = AccessLevels.ADMIN - elif invitation.accessLevelOld == 'rw': + elif invitation.accessLevelOld == "rw": invitation.accessLevel = AccessLevels.READ_WRITE - elif invitation.accessLevelOld == 'ro': + elif invitation.accessLevelOld == "ro": invitation.accessLevel = AccessLevels.READ_ONLY invitation.save() @@ -31,7 +31,7 @@ def change_access_level_to_int(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0027_auto_20200907_0752'), + ("django_etebase", "0027_auto_20200907_0752"), ] operations = [ diff --git a/django_etebase/migrations/0029_auto_20200907_0801.py b/django_etebase/migrations/0029_auto_20200907_0801.py index 7cd54d4..f3bfe61 100644 --- a/django_etebase/migrations/0029_auto_20200907_0801.py +++ b/django_etebase/migrations/0029_auto_20200907_0801.py @@ -6,16 +6,10 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0028_auto_20200907_0754'), + ("django_etebase", "0028_auto_20200907_0754"), ] operations = [ - migrations.RemoveField( - model_name='collectioninvitation', - name='accessLevelOld', - ), - migrations.RemoveField( - model_name='collectionmember', - name='accessLevelOld', - ), + migrations.RemoveField(model_name="collectioninvitation", name="accessLevelOld",), + migrations.RemoveField(model_name="collectionmember", name="accessLevelOld",), ] diff --git a/django_etebase/migrations/0030_auto_20200922_0832.py b/django_etebase/migrations/0030_auto_20200922_0832.py index d5fa95d..a689251 100644 --- a/django_etebase/migrations/0030_auto_20200922_0832.py +++ b/django_etebase/migrations/0030_auto_20200922_0832.py @@ -7,13 +7,18 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0029_auto_20200907_0801'), + ("django_etebase", "0029_auto_20200907_0801"), ] operations = [ migrations.AlterField( - model_name='collection', - name='main_item', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent', to='django_etebase.collectionitem'), + model_name="collection", + name="main_item", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="parent", + to="django_etebase.collectionitem", + ), ), ] diff --git a/django_etebase/migrations/0031_auto_20201013_1336.py b/django_etebase/migrations/0031_auto_20201013_1336.py index ca45dd4..ae6e5e5 100644 --- a/django_etebase/migrations/0031_auto_20201013_1336.py +++ b/django_etebase/migrations/0031_auto_20201013_1336.py @@ -9,21 +9,23 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('django_etebase', '0030_auto_20200922_0832'), + ("django_etebase", "0030_auto_20200922_0832"), ] operations = [ migrations.CreateModel( - name='CollectionType', + name="CollectionType", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uid', models.BinaryField(db_index=True, editable=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("uid", models.BinaryField(db_index=True, editable=True)), + ("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), migrations.AddField( - model_name='collectionmember', - name='collectionType', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='django_etebase.collectiontype'), + model_name="collectionmember", + name="collectionType", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.PROTECT, to="django_etebase.collectiontype" + ), ), ] diff --git a/django_etebase/migrations/0032_auto_20201013_1409.py b/django_etebase/migrations/0032_auto_20201013_1409.py index 5594006..2bb3cb0 100644 --- a/django_etebase/migrations/0032_auto_20201013_1409.py +++ b/django_etebase/migrations/0032_auto_20201013_1409.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('django_etebase', '0031_auto_20201013_1336'), + ("django_etebase", "0031_auto_20201013_1336"), ] operations = [ migrations.AlterField( - model_name='collectiontype', - name='uid', + model_name="collectiontype", + name="uid", field=models.BinaryField(db_index=True, editable=True, unique=True), ), ] diff --git a/django_etebase/models.py b/django_etebase/models.py index 0036884..691947d 100644 --- a/django_etebase/models.py +++ b/django_etebase/models.py @@ -27,7 +27,7 @@ from . import app_settings from .exceptions import EtebaseValidationError -UidValidator = RegexValidator(regex=r'^[a-zA-Z0-9\-_]{20,}$', message='Not a valid UID') +UidValidator = RegexValidator(regex=r"^[a-zA-Z0-9\-_]{20,}$", message="Not a valid UID") class CollectionType(models.Model): @@ -36,7 +36,7 @@ class CollectionType(models.Model): class Collection(models.Model): - main_item = models.OneToOneField('CollectionItem', related_name='parent', null=True, on_delete=models.SET_NULL) + main_item = models.OneToOneField("CollectionItem", related_name="parent", null=True, on_delete=models.SET_NULL) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) def __str__(self): @@ -56,38 +56,45 @@ class Collection(models.Model): @cached_property def stoken(self): - stoken = Stoken.objects.filter( - Q(collectionitemrevision__item__collection=self) | Q(collectionmember__collection=self) - ).order_by('id').last() + stoken = ( + Stoken.objects.filter( + Q(collectionitemrevision__item__collection=self) | Q(collectionmember__collection=self) + ) + .order_by("id") + .last() + ) if stoken is None: - raise Exception('stoken is None. Should never happen') + raise Exception("stoken is None. Should never happen") return stoken.uid def validate_unique(self, exclude=None): super().validate_unique(exclude=exclude) - if exclude is None or 'main_item' in exclude: + if exclude is None or "main_item" in exclude: return - if self.__class__.objects.filter(owner=self.owner, main_item__uid=self.main_item.uid) \ - .exclude(id=self.id).exists(): - raise EtebaseValidationError('unique_uid', 'Collection with this uid already exists', - status_code=status.HTTP_409_CONFLICT) + if ( + self.__class__.objects.filter(owner=self.owner, main_item__uid=self.main_item.uid) + .exclude(id=self.id) + .exists() + ): + raise EtebaseValidationError( + "unique_uid", "Collection with this uid already exists", status_code=status.HTTP_409_CONFLICT + ) class CollectionItem(models.Model): - uid = models.CharField(db_index=True, blank=False, - max_length=43, validators=[UidValidator]) - collection = models.ForeignKey(Collection, related_name='items', on_delete=models.CASCADE) + uid = models.CharField(db_index=True, blank=False, max_length=43, validators=[UidValidator]) + collection = models.ForeignKey(Collection, related_name="items", on_delete=models.CASCADE) version = models.PositiveSmallIntegerField() encryptionKey = models.BinaryField(editable=True, blank=False, null=True) class Meta: - unique_together = ('uid', 'collection') + unique_together = ("uid", "collection") def __str__(self): - return '{} {}'.format(self.uid, self.collection.uid) + return "{} {}".format(self.uid, self.collection.uid) @cached_property def content(self): @@ -107,53 +114,60 @@ def chunk_directory_path(instance, filename): user_id = col.owner.id uid_prefix = instance.uid[:2] uid_rest = instance.uid[2:] - return Path('user_{}'.format(user_id), col.uid, uid_prefix, uid_rest) + return Path("user_{}".format(user_id), col.uid, uid_prefix, uid_rest) class CollectionItemChunk(models.Model): - uid = models.CharField(db_index=True, blank=False, null=False, - max_length=60, validators=[UidValidator]) - collection = models.ForeignKey(Collection, related_name='chunks', on_delete=models.CASCADE) + uid = models.CharField(db_index=True, blank=False, null=False, max_length=60, validators=[UidValidator]) + collection = models.ForeignKey(Collection, related_name="chunks", on_delete=models.CASCADE) chunkFile = models.FileField(upload_to=chunk_directory_path, max_length=150, unique=True) def __str__(self): return self.uid class Meta: - unique_together = ('collection', 'uid') + unique_together = ("collection", "uid") def generate_stoken_uid(): - return get_random_string(32, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_') + return get_random_string(32, allowed_chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_") class Stoken(models.Model): - uid = models.CharField(db_index=True, unique=True, blank=False, null=False, default=generate_stoken_uid, - max_length=43, validators=[UidValidator]) + uid = models.CharField( + db_index=True, + unique=True, + blank=False, + null=False, + default=generate_stoken_uid, + max_length=43, + validators=[UidValidator], + ) class CollectionItemRevision(models.Model): stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT) - uid = models.CharField(db_index=True, unique=True, blank=False, null=False, - max_length=43, validators=[UidValidator]) - item = models.ForeignKey(CollectionItem, related_name='revisions', on_delete=models.CASCADE) + uid = models.CharField( + db_index=True, unique=True, blank=False, null=False, max_length=43, validators=[UidValidator] + ) + item = models.ForeignKey(CollectionItem, related_name="revisions", on_delete=models.CASCADE) meta = models.BinaryField(editable=True, blank=False, null=False) current = models.BooleanField(db_index=True, default=True, null=True) deleted = models.BooleanField(default=False) class Meta: - unique_together = ('item', 'current') + unique_together = ("item", "current") def __str__(self): - return '{} {} current={}'.format(self.uid, self.item.uid, self.current) + return "{} {} current={}".format(self.uid, self.item.uid, self.current) class RevisionChunkRelation(models.Model): - chunk = models.ForeignKey(CollectionItemChunk, related_name='revisions_relation', on_delete=models.CASCADE) - revision = models.ForeignKey(CollectionItemRevision, related_name='chunks_relation', on_delete=models.CASCADE) + chunk = models.ForeignKey(CollectionItemChunk, related_name="revisions_relation", on_delete=models.CASCADE) + revision = models.ForeignKey(CollectionItemRevision, related_name="chunks_relation", on_delete=models.CASCADE) class Meta: - ordering = ('id', ) + ordering = ("id",) class AccessLevels(models.IntegerChoices): @@ -164,28 +178,22 @@ class AccessLevels(models.IntegerChoices): class CollectionMember(models.Model): stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT, null=True) - collection = models.ForeignKey(Collection, related_name='members', on_delete=models.CASCADE) + collection = models.ForeignKey(Collection, related_name="members", on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) encryptionKey = models.BinaryField(editable=True, blank=False, null=False) collectionType = models.ForeignKey(CollectionType, on_delete=models.PROTECT, null=True) - accessLevel = models.IntegerField( - choices=AccessLevels.choices, - default=AccessLevels.READ_ONLY, - ) + accessLevel = models.IntegerField(choices=AccessLevels.choices, default=AccessLevels.READ_ONLY,) class Meta: - unique_together = ('user', 'collection') + unique_together = ("user", "collection") def __str__(self): - return '{} {}'.format(self.collection.uid, self.user) + return "{} {}".format(self.collection.uid, self.user) def revoke(self): with transaction.atomic(): CollectionMemberRemoved.objects.update_or_create( - collection=self.collection, user=self.user, - defaults={ - 'stoken': Stoken.objects.create(), - }, + collection=self.collection, user=self.user, defaults={"stoken": Stoken.objects.create(),}, ) self.delete() @@ -193,36 +201,32 @@ class CollectionMember(models.Model): class CollectionMemberRemoved(models.Model): stoken = models.OneToOneField(Stoken, on_delete=models.PROTECT, null=True) - collection = models.ForeignKey(Collection, related_name='removed_members', on_delete=models.CASCADE) + collection = models.ForeignKey(Collection, related_name="removed_members", on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) class Meta: - unique_together = ('user', 'collection') + unique_together = ("user", "collection") def __str__(self): - return '{} {}'.format(self.collection.uid, self.user) + return "{} {}".format(self.collection.uid, self.user) class CollectionInvitation(models.Model): - uid = models.CharField(db_index=True, blank=False, null=False, - max_length=43, validators=[UidValidator]) + uid = models.CharField(db_index=True, blank=False, null=False, max_length=43, validators=[UidValidator]) version = models.PositiveSmallIntegerField(default=1) fromMember = models.ForeignKey(CollectionMember, on_delete=models.CASCADE) # FIXME: make sure to delete all invitations for the same collection once one is accepted # Make sure to not allow invitations if already a member - user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='incoming_invitations', on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="incoming_invitations", on_delete=models.CASCADE) signedEncryptionKey = models.BinaryField(editable=False, blank=False, null=False) - accessLevel = models.IntegerField( - choices=AccessLevels.choices, - default=AccessLevels.READ_ONLY, - ) + accessLevel = models.IntegerField(choices=AccessLevels.choices, default=AccessLevels.READ_ONLY,) class Meta: - unique_together = ('user', 'fromMember') + unique_together = ("user", "fromMember") def __str__(self): - return '{} {}'.format(self.fromMember.collection.uid, self.user) + return "{} {}".format(self.fromMember.collection.uid, self.user) @cached_property def collection(self): diff --git a/django_etebase/parsers.py b/django_etebase/parsers.py index 1ca1a70..c7fe58c 100644 --- a/django_etebase/parsers.py +++ b/django_etebase/parsers.py @@ -5,11 +5,12 @@ class ChunkUploadParser(FileUploadParser): """ Parser for chunk upload data. """ - media_type = 'application/octet-stream' + + media_type = "application/octet-stream" def get_filename(self, stream, media_type, parser_context): """ Detects the uploaded file name. """ - view = parser_context['view'] - return parser_context['kwargs'][view.lookup_field] + view = parser_context["view"] + return parser_context["kwargs"][view.lookup_field] diff --git a/django_etebase/permissions.py b/django_etebase/permissions.py index c624404..3c77d06 100644 --- a/django_etebase/permissions.py +++ b/django_etebase/permissions.py @@ -25,13 +25,14 @@ class IsCollectionAdmin(permissions.BasePermission): """ Custom permission to only allow owners of a collection to view it """ + message = { - 'detail': 'Only collection admins can perform this operation.', - 'code': 'admin_access_required', + "detail": "Only collection admins can perform this operation.", + "code": "admin_access_required", } def has_permission(self, request, view): - collection_uid = view.kwargs['collection_uid'] + collection_uid = view.kwargs["collection_uid"] try: collection = view.get_collection_queryset().get(main_item__uid=collection_uid) return is_collection_admin(collection, request.user) @@ -44,13 +45,14 @@ class IsCollectionAdminOrReadOnly(permissions.BasePermission): """ Custom permission to only allow owners of a collection to edit it """ + message = { - 'detail': 'Only collection admins can edit collections.', - 'code': 'admin_access_required', + "detail": "Only collection admins can edit collections.", + "code": "admin_access_required", } def has_permission(self, request, view): - collection_uid = view.kwargs.get('collection_uid', None) + collection_uid = view.kwargs.get("collection_uid", None) # Allow creating new collections if collection_uid is None: @@ -71,13 +73,14 @@ class HasWriteAccessOrReadOnly(permissions.BasePermission): """ Custom permission to restrict write """ + message = { - 'detail': 'You need write access to write to this collection', - 'code': 'no_write_access', + "detail": "You need write access to write to this collection", + "code": "no_write_access", } def has_permission(self, request, view): - collection_uid = view.kwargs['collection_uid'] + collection_uid = view.kwargs["collection_uid"] try: collection = view.get_collection_queryset().get(main_item__uid=collection_uid) if request.method in permissions.SAFE_METHODS: diff --git a/django_etebase/renderers.py b/django_etebase/renderers.py index 43c1a0d..0d359d3 100644 --- a/django_etebase/renderers.py +++ b/django_etebase/renderers.py @@ -15,4 +15,5 @@ class JSONRenderer(DRFJSONRenderer): """ Renderer which serializes to JSON with support for our base64 """ + encoder_class = JSONEncoder diff --git a/django_etebase/serializers.py b/django_etebase/serializers.py index 4f2d802..ef3b296 100644 --- a/django_etebase/serializers.py +++ b/django_etebase/serializers.py @@ -29,7 +29,7 @@ User = get_user_model() def process_revisions_for_item(item, revision_data): chunks_objs = [] - chunks = revision_data.pop('chunks_relation') + chunks = revision_data.pop("chunks_relation") revision = models.CollectionItemRevision(**revision_data, item=item) revision.validate_unique() # Verify there aren't any validation issues @@ -42,11 +42,11 @@ def process_revisions_for_item(item, revision_data): # If the chunk already exists we assume it's fine. Otherwise, we upload it. if chunk_obj is None: chunk_obj = models.CollectionItemChunk(uid=uid, collection=item.collection) - chunk_obj.chunkFile.save('IGNORED', ContentFile(content)) + chunk_obj.chunkFile.save("IGNORED", ContentFile(content)) chunk_obj.save() else: if chunk_obj is None: - raise EtebaseValidationError('chunk_no_content', 'Tried to create a new chunk without content') + raise EtebaseValidationError("chunk_no_content", "Tried to create a new chunk without content") chunks_objs.append(chunk_obj) @@ -60,7 +60,7 @@ def process_revisions_for_item(item, revision_data): def b64encode(value): - return base64.urlsafe_b64encode(value).decode('ascii').strip('=') + return base64.urlsafe_b64encode(value).decode("ascii").strip("=") def b64decode(data): @@ -85,7 +85,7 @@ class BinaryBase64Field(serializers.Field): class CollectionEncryptionKeyField(BinaryBase64Field): def get_attribute(self, instance): - request = self.context.get('request', None) + request = self.context.get("request", None) if request is not None: return instance.members.get(user=request.user).encryptionKey return None @@ -93,7 +93,7 @@ class CollectionEncryptionKeyField(BinaryBase64Field): class CollectionTypeField(BinaryBase64Field): def get_attribute(self, instance): - request = self.context.get('request', None) + request = self.context.get("request", None) if request is not None: collection_type = instance.members.get(user=request.user).collectionType return collection_type and collection_type.uid @@ -102,7 +102,7 @@ class CollectionTypeField(BinaryBase64Field): class UserSlugRelatedField(serializers.SlugRelatedField): def get_queryset(self): - view = self.context.get('view', None) + view = self.context.get("view", None) return get_user_queryset(super().get_queryset(), view) def __init__(self, **kwargs): @@ -115,15 +115,15 @@ class UserSlugRelatedField(serializers.SlugRelatedField): class ChunksField(serializers.RelatedField): def to_representation(self, obj): obj = obj.chunk - if self.context.get('prefetch') == 'auto': - with open(obj.chunkFile.path, 'rb') as f: + if self.context.get("prefetch") == "auto": + with open(obj.chunkFile.path, "rb") as f: return (obj.uid, f.read()) else: - return (obj.uid, ) + return (obj.uid,) def to_internal_value(self, data): if data[0] is None or data[1] is None: - raise EtebaseValidationError('no_null', 'null is not allowed') + raise EtebaseValidationError("no_null", "null is not allowed") return (data[0], b64decode_or_bytes(data[1])) @@ -133,18 +133,12 @@ class BetterErrorsMixin: nice = [] errors = super().errors for error_type in errors: - if error_type == 'non_field_errors': - nice.extend( - self.flatten_errors(None, errors[error_type]) - ) + if error_type == "non_field_errors": + nice.extend(self.flatten_errors(None, errors[error_type])) else: - nice.extend( - self.flatten_errors(error_type, errors[error_type]) - ) + nice.extend(self.flatten_errors(error_type, errors[error_type])) if nice: - return {'code': 'field_errors', - 'detail': 'Field validations failed.', - 'errors': nice} + return {"code": "field_errors", "detail": "Field validations failed.", "errors": nice} return {} def flatten_errors(self, field_name, errors): @@ -155,54 +149,50 @@ class BetterErrorsMixin: ret.extend(self.flatten_errors("{}.{}".format(field_name, error_key), error)) else: for error in errors: - if hasattr(error, 'detail'): + if hasattr(error, "detail"): message = error.detail[0] - elif hasattr(error, 'message'): + elif hasattr(error, "message"): message = error.message else: message = str(error) - ret.append({ - 'field': field_name, - 'code': error.code, - 'detail': message, - }) + ret.append( + {"field": field_name, "code": error.code, "detail": message,} + ) return ret def transform_validation_error(self, prefix, err): - if hasattr(err, 'error_dict'): + if hasattr(err, "error_dict"): errors = self.flatten_errors(prefix, err.error_dict) - elif not hasattr(err, 'message'): + elif not hasattr(err, "message"): errors = self.flatten_errors(prefix, err.error_list) else: raise EtebaseValidationError(err.code, err.message) - raise serializers.ValidationError({ - 'code': 'field_errors', - 'detail': 'Field validations failed.', - 'errors': errors, - }) + raise serializers.ValidationError( + {"code": "field_errors", "detail": "Field validations failed.", "errors": errors,} + ) class CollectionItemChunkSerializer(BetterErrorsMixin, serializers.ModelSerializer): class Meta: model = models.CollectionItemChunk - fields = ('uid', 'chunkFile') + fields = ("uid", "chunkFile") class CollectionItemRevisionSerializer(BetterErrorsMixin, serializers.ModelSerializer): chunks = ChunksField( - source='chunks_relation', + source="chunks_relation", queryset=models.RevisionChunkRelation.objects.all(), - style={'base_template': 'input.html'}, - many=True + style={"base_template": "input.html"}, + many=True, ) meta = BinaryBase64Field() class Meta: model = models.CollectionItemRevision - fields = ('chunks', 'meta', 'uid', 'deleted') + fields = ("chunks", "meta", "uid", "deleted") extra_kwargs = { - 'uid': {'validators': []}, # We deal with it in the serializers + "uid": {"validators": []}, # We deal with it in the serializers } @@ -213,14 +203,14 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer): class Meta: model = models.CollectionItem - fields = ('uid', 'version', 'encryptionKey', 'content', 'etag') + fields = ("uid", "version", "encryptionKey", "content", "etag") def create(self, validated_data): """Function that's called when this serializer creates an item""" - validate_etag = self.context.get('validate_etag', False) - etag = validated_data.pop('etag') - revision_data = validated_data.pop('content') - uid = validated_data.pop('uid') + validate_etag = self.context.get("validate_etag", False) + etag = validated_data.pop("etag") + revision_data = validated_data.pop("content") + uid = validated_data.pop("uid") Model = self.__class__.Meta.model @@ -229,12 +219,15 @@ class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer): cur_etag = instance.etag if not created else None # If we are trying to update an up to date item, abort early and consider it a success - if cur_etag == revision_data.get('uid'): + if cur_etag == revision_data.get("uid"): return instance if validate_etag and cur_etag != etag: - raise EtebaseValidationError('wrong_etag', 'Wrong etag. Expected {} got {}'.format(cur_etag, etag), - status_code=status.HTTP_409_CONFLICT) + raise EtebaseValidationError( + "wrong_etag", + "Wrong etag. Expected {} got {}".format(cur_etag, etag), + status_code=status.HTTP_409_CONFLICT, + ) if not created: # We don't have to use select_for_update here because the unique constraint on current guards against @@ -260,14 +253,17 @@ class CollectionItemDepSerializer(BetterErrorsMixin, serializers.ModelSerializer class Meta: model = models.CollectionItem - fields = ('uid', 'etag') + fields = ("uid", "etag") def validate(self, data): - item = self.__class__.Meta.model.objects.get(uid=data['uid']) - etag = data['etag'] + item = self.__class__.Meta.model.objects.get(uid=data["uid"]) + etag = data["etag"] if item.etag != etag: - raise EtebaseValidationError('wrong_etag', 'Wrong etag. Expected {} got {}'.format(item.etag, etag), - status_code=status.HTTP_409_CONFLICT) + raise EtebaseValidationError( + "wrong_etag", + "Wrong etag. Expected {} got {}".format(item.etag, etag), + status_code=status.HTTP_409_CONFLICT, + ) return data @@ -277,49 +273,47 @@ class CollectionItemBulkGetSerializer(BetterErrorsMixin, serializers.ModelSerial class Meta: model = models.CollectionItem - fields = ('uid', 'etag') + fields = ("uid", "etag") class CollectionListMultiSerializer(BetterErrorsMixin, serializers.Serializer): - collectionTypes = serializers.ListField( - child=BinaryBase64Field() - ) + collectionTypes = serializers.ListField(child=BinaryBase64Field()) class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer): collectionKey = CollectionEncryptionKeyField() collectionType = CollectionTypeField() - accessLevel = serializers.SerializerMethodField('get_access_level_from_context') + accessLevel = serializers.SerializerMethodField("get_access_level_from_context") stoken = serializers.CharField(read_only=True) - item = CollectionItemSerializer(many=False, source='main_item') + item = CollectionItemSerializer(many=False, source="main_item") class Meta: model = models.Collection - fields = ('item', 'accessLevel', 'collectionKey', 'collectionType', 'stoken') + fields = ("item", "accessLevel", "collectionKey", "collectionType", "stoken") def get_access_level_from_context(self, obj): - request = self.context.get('request', None) + request = self.context.get("request", None) if request is not None: return obj.members.get(user=request.user).accessLevel return None def create(self, validated_data): """Function that's called when this serializer creates an item""" - collection_key = validated_data.pop('collectionKey') - collection_type = validated_data.pop('collectionType') + collection_key = validated_data.pop("collectionKey") + collection_type = validated_data.pop("collectionType") - user = validated_data.get('owner') - main_item_data = validated_data.pop('main_item') - etag = main_item_data.pop('etag') - revision_data = main_item_data.pop('content') + user = validated_data.get("owner") + main_item_data = validated_data.pop("main_item") + etag = main_item_data.pop("etag") + revision_data = main_item_data.pop("content") instance = self.__class__.Meta.model(**validated_data) with transaction.atomic(): _ = self.__class__.Meta.model.objects.select_for_update().filter(owner=user) if etag is not None: - raise EtebaseValidationError('bad_etag', 'etag is not null') + raise EtebaseValidationError("bad_etag", "etag is not null") instance.save() main_item = models.CollectionItem.objects.create(**main_item_data, collection=instance) @@ -333,13 +327,14 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer): collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user) - models.CollectionMember(collection=instance, - stoken=models.Stoken.objects.create(), - user=user, - accessLevel=models.AccessLevels.ADMIN, - encryptionKey=collection_key, - collectionType=collection_type_obj, - ).save() + models.CollectionMember( + collection=instance, + stoken=models.Stoken.objects.create(), + user=user, + accessLevel=models.AccessLevels.ADMIN, + encryptionKey=collection_key, + collectionType=collection_type_obj, + ).save() return instance @@ -348,15 +343,11 @@ class CollectionSerializer(BetterErrorsMixin, serializers.ModelSerializer): class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer): - username = UserSlugRelatedField( - source='user', - read_only=True, - style={'base_template': 'input.html'}, - ) + username = UserSlugRelatedField(source="user", read_only=True, style={"base_template": "input.html"},) class Meta: model = models.CollectionMember - fields = ('username', 'accessLevel') + fields = ("username", "accessLevel") def create(self, validated_data): raise NotImplementedError() @@ -364,7 +355,7 @@ class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer) def update(self, instance, validated_data): with transaction.atomic(): # We only allow updating accessLevel - access_level = validated_data.pop('accessLevel') + access_level = validated_data.pop("accessLevel") if instance.accessLevel != access_level: instance.stoken = models.Stoken.objects.create() instance.accessLevel = access_level @@ -374,31 +365,35 @@ class CollectionMemberSerializer(BetterErrorsMixin, serializers.ModelSerializer) class CollectionInvitationSerializer(BetterErrorsMixin, serializers.ModelSerializer): - username = UserSlugRelatedField( - source='user', - queryset=User.objects, - style={'base_template': 'input.html'}, - ) - collection = serializers.CharField(source='collection.uid') - fromUsername = BinaryBase64Field(source='fromMember.user.username', read_only=True) - fromPubkey = BinaryBase64Field(source='fromMember.user.userinfo.pubkey', read_only=True) + username = UserSlugRelatedField(source="user", queryset=User.objects, style={"base_template": "input.html"},) + collection = serializers.CharField(source="collection.uid") + fromUsername = BinaryBase64Field(source="fromMember.user.username", read_only=True) + fromPubkey = BinaryBase64Field(source="fromMember.user.userinfo.pubkey", read_only=True) signedEncryptionKey = BinaryBase64Field() class Meta: model = models.CollectionInvitation - fields = ('username', 'uid', 'collection', 'signedEncryptionKey', 'accessLevel', - 'fromUsername', 'fromPubkey', 'version') + fields = ( + "username", + "uid", + "collection", + "signedEncryptionKey", + "accessLevel", + "fromUsername", + "fromPubkey", + "version", + ) def validate_user(self, value): - request = self.context['request'] + request = self.context["request"] if request.user.username == value.lower(): - raise EtebaseValidationError('no_self_invite', 'Inviting yourself is not allowed') + raise EtebaseValidationError("no_self_invite", "Inviting yourself is not allowed") return value def create(self, validated_data): - request = self.context['request'] - collection = validated_data.pop('collection') + request = self.context["request"] + collection = validated_data.pop("collection") member = collection.members.get(user=request.user) @@ -406,12 +401,12 @@ class CollectionInvitationSerializer(BetterErrorsMixin, serializers.ModelSeriali try: return type(self).Meta.model.objects.create(**validated_data, fromMember=member) except IntegrityError: - raise EtebaseValidationError('invitation_exists', 'Invitation already exists') + raise EtebaseValidationError("invitation_exists", "Invitation already exists") def update(self, instance, validated_data): with transaction.atomic(): - instance.accessLevel = validated_data.pop('accessLevel') - instance.signedEncryptionKey = validated_data.pop('signedEncryptionKey') + instance.accessLevel = validated_data.pop("accessLevel") + instance.signedEncryptionKey = validated_data.pop("signedEncryptionKey") instance.save() return instance @@ -424,9 +419,9 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer): def create(self, validated_data): with transaction.atomic(): - invitation = self.context['invitation'] - encryption_key = validated_data.get('encryptionKey') - collection_type = validated_data.pop('collectionType') + invitation = self.context["invitation"] + encryption_key = validated_data.get("encryptionKey") + collection_type = validated_data.pop("collectionType") user = invitation.user collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=collection_type, owner=user) @@ -438,10 +433,11 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer): accessLevel=invitation.accessLevel, encryptionKey=encryption_key, collectionType=collection_type_obj, - ) + ) models.CollectionMemberRemoved.objects.filter( - user=invitation.user, collection=invitation.collection).delete() + user=invitation.user, collection=invitation.collection + ).delete() invitation.delete() @@ -452,12 +448,12 @@ class InvitationAcceptSerializer(BetterErrorsMixin, serializers.Serializer): class UserSerializer(BetterErrorsMixin, serializers.ModelSerializer): - pubkey = BinaryBase64Field(source='userinfo.pubkey') - encryptedContent = BinaryBase64Field(source='userinfo.encryptedContent') + pubkey = BinaryBase64Field(source="userinfo.pubkey") + encryptedContent = BinaryBase64Field(source="userinfo.encryptedContent") class Meta: model = User - fields = (User.USERNAME_FIELD, User.EMAIL_FIELD, 'pubkey', 'encryptedContent') + fields = (User.USERNAME_FIELD, User.EMAIL_FIELD, "pubkey", "encryptedContent") class UserInfoPubkeySerializer(BetterErrorsMixin, serializers.ModelSerializer): @@ -465,7 +461,7 @@ class UserInfoPubkeySerializer(BetterErrorsMixin, serializers.ModelSerializer): class Meta: model = models.UserInfo - fields = ('pubkey', ) + fields = ("pubkey",) class UserSignupSerializer(BetterErrorsMixin, serializers.ModelSerializer): @@ -473,7 +469,7 @@ class UserSignupSerializer(BetterErrorsMixin, serializers.ModelSerializer): model = User fields = (User.USERNAME_FIELD, User.EMAIL_FIELD) extra_kwargs = { - 'username': {'validators': []}, # We specifically validate in SignupSerializer + "username": {"validators": []}, # We specifically validate in SignupSerializer } @@ -481,6 +477,7 @@ class AuthenticationSignupSerializer(BetterErrorsMixin, serializers.Serializer): """Used both for creating new accounts and setting up existing ones for the first time. When setting up existing ones the email is ignored." """ + user = UserSignupSerializer(many=False) salt = BinaryBase64Field() loginPubkey = BinaryBase64Field() @@ -489,27 +486,27 @@ class AuthenticationSignupSerializer(BetterErrorsMixin, serializers.Serializer): def create(self, validated_data): """Function that's called when this serializer creates an item""" - user_data = validated_data.pop('user') + user_data = validated_data.pop("user") with transaction.atomic(): try: - view = self.context.get('view', None) + view = self.context.get("view", None) user_queryset = get_user_queryset(User.objects.all(), view) - instance = user_queryset.get(**{User.USERNAME_FIELD: user_data['username'].lower()}) + instance = user_queryset.get(**{User.USERNAME_FIELD: user_data["username"].lower()}) except User.DoesNotExist: # Create the user and save the casing the user chose as the first name try: - instance = create_user(**user_data, password=None, first_name=user_data['username'], view=view) + instance = create_user(**user_data, password=None, first_name=user_data["username"], view=view) instance.clean_fields() except EtebaseValidationError as e: raise e except django_exceptions.ValidationError as e: self.transform_validation_error("user", e) except Exception as e: - raise EtebaseValidationError('generic', str(e)) + raise EtebaseValidationError("generic", str(e)) - if hasattr(instance, 'userinfo'): - raise EtebaseValidationError('user_exists', 'User already exists', status_code=status.HTTP_409_CONFLICT) + if hasattr(instance, "userinfo"): + raise EtebaseValidationError("user_exists", "User already exists", status_code=status.HTTP_409_CONFLICT) models.UserInfo.objects.create(**validated_data, owner=instance) @@ -558,15 +555,15 @@ class AuthenticationChangePasswordInnerSerializer(AuthenticationLoginInnerSerial class Meta: model = models.UserInfo - fields = ('loginPubkey', 'encryptedContent') + fields = ("loginPubkey", "encryptedContent") def create(self, validated_data): raise NotImplementedError() def update(self, instance, validated_data): with transaction.atomic(): - instance.loginPubkey = validated_data.pop('loginPubkey') - instance.encryptedContent = validated_data.pop('encryptedContent') + instance.loginPubkey = validated_data.pop("loginPubkey") + instance.encryptedContent = validated_data.pop("encryptedContent") instance.save() return instance diff --git a/django_etebase/signals.py b/django_etebase/signals.py index 03dbed5..0fc3e80 100644 --- a/django_etebase/signals.py +++ b/django_etebase/signals.py @@ -1,3 +1,3 @@ from django.dispatch import Signal -user_signed_up = Signal(providing_args=['request', 'user']) +user_signed_up = Signal(providing_args=["request", "user"]) diff --git a/django_etebase/token_auth/apps.py b/django_etebase/token_auth/apps.py index 118b872..a0e98be 100644 --- a/django_etebase/token_auth/apps.py +++ b/django_etebase/token_auth/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class TokenAuthConfig(AppConfig): - name = 'django_etebase.token_auth' + name = "django_etebase.token_auth" diff --git a/django_etebase/token_auth/authentication.py b/django_etebase/token_auth/authentication.py index 432c8cf..7e84956 100644 --- a/django_etebase/token_auth/authentication.py +++ b/django_etebase/token_auth/authentication.py @@ -12,19 +12,19 @@ MIN_REFRESH_INTERVAL = 60 class TokenAuthentication(DRFTokenAuthentication): - keyword = 'Token' + keyword = "Token" model = AuthToken def authenticate_credentials(self, key): - msg = _('Invalid token.') + msg = _("Invalid token.") model = self.get_model() try: - token = model.objects.select_related('user').get(key=key) + token = model.objects.select_related("user").get(key=key) except model.DoesNotExist: raise exceptions.AuthenticationFailed(msg) if not token.user.is_active: - raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + raise exceptions.AuthenticationFailed(_("User inactive or deleted.")) if token.expiry is not None: if token.expiry < timezone.now(): @@ -43,4 +43,4 @@ class TokenAuthentication(DRFTokenAuthentication): delta = (new_expiry - current_expiry).total_seconds() if delta > MIN_REFRESH_INTERVAL: auth_token.expiry = new_expiry - auth_token.save(update_fields=('expiry',)) + auth_token.save(update_fields=("expiry",)) diff --git a/django_etebase/token_auth/migrations/0001_initial.py b/django_etebase/token_auth/migrations/0001_initial.py index 5a47366..660b38c 100644 --- a/django_etebase/token_auth/migrations/0001_initial.py +++ b/django_etebase/token_auth/migrations/0001_initial.py @@ -16,13 +16,23 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='AuthToken', + name="AuthToken", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(db_index=True, default=token_auth_models.generate_key, max_length=40, unique=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('expiry', models.DateTimeField(blank=True, default=token_auth_models.get_default_expiry, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auth_token_set', to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "key", + models.CharField(db_index=True, default=token_auth_models.generate_key, max_length=40, unique=True), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("expiry", models.DateTimeField(blank=True, default=token_auth_models.get_default_expiry, null=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="auth_token_set", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/django_etebase/token_auth/models.py b/django_etebase/token_auth/models.py index 0fe4766..ac1efff 100644 --- a/django_etebase/token_auth/models.py +++ b/django_etebase/token_auth/models.py @@ -17,10 +17,9 @@ def get_default_expiry(): class AuthToken(models.Model): key = models.CharField(max_length=40, unique=True, db_index=True, default=generate_key) - user = models.ForeignKey(User, null=False, blank=False, - related_name='auth_token_set', on_delete=models.CASCADE) + user = models.ForeignKey(User, null=False, blank=False, related_name="auth_token_set", on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) expiry = models.DateTimeField(null=True, blank=True, default=get_default_expiry) def __str__(self): - return '{}: {}'.format(self.key, self.user) + return "{}: {}".format(self.key, self.user) diff --git a/django_etebase/urls.py b/django_etebase/urls.py index f6d982e..01797c1 100644 --- a/django_etebase/urls.py +++ b/django_etebase/urls.py @@ -7,24 +7,24 @@ from rest_framework_nested import routers from django_etebase import views router = routers.DefaultRouter() -router.register(r'collection', views.CollectionViewSet) -router.register(r'authentication', views.AuthenticationViewSet, basename='authentication') -router.register(r'invitation/incoming', views.InvitationIncomingViewSet, basename='invitation_incoming') -router.register(r'invitation/outgoing', views.InvitationOutgoingViewSet, basename='invitation_outgoing') +router.register(r"collection", views.CollectionViewSet) +router.register(r"authentication", views.AuthenticationViewSet, basename="authentication") +router.register(r"invitation/incoming", views.InvitationIncomingViewSet, basename="invitation_incoming") +router.register(r"invitation/outgoing", views.InvitationOutgoingViewSet, basename="invitation_outgoing") -collections_router = routers.NestedSimpleRouter(router, r'collection', lookup='collection') -collections_router.register(r'item', views.CollectionItemViewSet, basename='collection_item') -collections_router.register(r'member', views.CollectionMemberViewSet, basename='collection_member') +collections_router = routers.NestedSimpleRouter(router, r"collection", lookup="collection") +collections_router.register(r"item", views.CollectionItemViewSet, basename="collection_item") +collections_router.register(r"member", views.CollectionMemberViewSet, basename="collection_member") -item_router = routers.NestedSimpleRouter(collections_router, r'item', lookup='collection_item') -item_router.register(r'chunk', views.CollectionItemChunkViewSet, basename='collection_items_chunk') +item_router = routers.NestedSimpleRouter(collections_router, r"item", lookup="collection_item") +item_router.register(r"chunk", views.CollectionItemChunkViewSet, basename="collection_items_chunk") if settings.DEBUG: - router.register(r'test/authentication', views.TestAuthenticationViewSet, basename='test_authentication') + router.register(r"test/authentication", views.TestAuthenticationViewSet, basename="test_authentication") -app_name = 'django_etebase' +app_name = "django_etebase" urlpatterns = [ - path('v1/', include(router.urls)), - path('v1/', include(collections_router.urls)), - path('v1/', include(item_router.urls)), + path("v1/", include(router.urls)), + path("v1/", include(collections_router.urls)), + path("v1/", include(item_router.urls)), ] diff --git a/django_etebase/utils.py b/django_etebase/utils.py index 1351f9b..e496a77 100644 --- a/django_etebase/utils.py +++ b/django_etebase/utils.py @@ -18,9 +18,9 @@ def create_user(*args, **kwargs): custom_func = app_settings.CREATE_USER_FUNC if custom_func is not None: return custom_func(*args, **kwargs) - _ = kwargs.pop('view') + _ = kwargs.pop("view") return User.objects.create_user(*args, **kwargs) def create_user_blocked(*args, **kwargs): - raise PermissionDenied('Signup is disabled for this server. Please refer to the README for more information.') + raise PermissionDenied("Signup is disabled for this server. Please refer to the README for more information.") diff --git a/django_etebase/views.py b/django_etebase/views.py index 2dc7adf..7dd7526 100644 --- a/django_etebase/views.py +++ b/django_etebase/views.py @@ -45,34 +45,34 @@ from .drf_msgpack.renderers import MessagePackRenderer from . import app_settings, permissions from .renderers import JSONRenderer from .models import ( - Collection, - CollectionItem, - CollectionItemRevision, - CollectionMember, - CollectionMemberRemoved, - CollectionInvitation, - Stoken, - UserInfo, - ) + Collection, + CollectionItem, + CollectionItemRevision, + CollectionMember, + CollectionMemberRemoved, + CollectionInvitation, + Stoken, + UserInfo, +) from .serializers import ( - AuthenticationChangePasswordInnerSerializer, - AuthenticationSignupSerializer, - AuthenticationLoginChallengeSerializer, - AuthenticationLoginSerializer, - AuthenticationLoginInnerSerializer, - CollectionSerializer, - CollectionItemSerializer, - CollectionItemBulkGetSerializer, - CollectionItemDepSerializer, - CollectionItemRevisionSerializer, - CollectionItemChunkSerializer, - CollectionListMultiSerializer, - CollectionMemberSerializer, - CollectionInvitationSerializer, - InvitationAcceptSerializer, - UserInfoPubkeySerializer, - UserSerializer, - ) + AuthenticationChangePasswordInnerSerializer, + AuthenticationSignupSerializer, + AuthenticationLoginChallengeSerializer, + AuthenticationLoginSerializer, + AuthenticationLoginInnerSerializer, + CollectionSerializer, + CollectionItemSerializer, + CollectionItemBulkGetSerializer, + CollectionItemDepSerializer, + CollectionItemRevisionSerializer, + CollectionItemChunkSerializer, + CollectionListMultiSerializer, + CollectionMemberSerializer, + CollectionInvitationSerializer, + InvitationAcceptSerializer, + UserInfoPubkeySerializer, + UserSerializer, +) from .utils import get_user_queryset from .exceptions import EtebaseValidationError from .parsers import ChunkUploadParser @@ -99,8 +99,8 @@ class BaseViewSet(viewsets.ModelViewSet): def get_serializer_class(self): serializer_class = self.serializer_class - if self.request.method == 'PUT': - serializer_class = getattr(self, 'serializer_update_class', serializer_class) + if self.request.method == "PUT": + serializer_class = getattr(self, "serializer_update_class", serializer_class) return serializer_class @@ -109,7 +109,7 @@ class BaseViewSet(viewsets.ModelViewSet): return queryset.filter(members__user=user) def get_stoken_obj_id(self, request): - return request.GET.get('stoken', None) + return request.GET.get("stoken", None) def get_stoken_obj(self, request): stoken = self.get_stoken_obj_id(request) @@ -118,7 +118,7 @@ class BaseViewSet(viewsets.ModelViewSet): try: return Stoken.objects.get(uid=stoken) except Stoken.DoesNotExist: - raise EtebaseValidationError('bad_stoken', 'Invalid stoken.', status_code=status.HTTP_400_BAD_REQUEST) + raise EtebaseValidationError("bad_stoken", "Invalid stoken.", status_code=status.HTTP_400_BAD_REQUEST) return None @@ -127,7 +127,7 @@ class BaseViewSet(viewsets.ModelViewSet): aggr_fields = [Coalesce(Max(field), V(0)) for field in self.stoken_id_fields] max_stoken = Greatest(*aggr_fields) if len(aggr_fields) > 1 else aggr_fields[0] - queryset = queryset.annotate(max_stoken=max_stoken).order_by('max_stoken') + queryset = queryset.annotate(max_stoken=max_stoken).order_by("max_stoken") if stoken_rev is not None: queryset = queryset.filter(max_stoken__gt=stoken_rev.id) @@ -137,18 +137,18 @@ class BaseViewSet(viewsets.ModelViewSet): def get_queryset_stoken(self, queryset): maxid = -1 for row in queryset: - rowmaxid = getattr(row, 'max_stoken') or -1 + rowmaxid = getattr(row, "max_stoken") or -1 maxid = max(maxid, rowmaxid) new_stoken = (maxid >= 0) and Stoken.objects.get(id=maxid) return new_stoken or None def filter_by_stoken_and_limit(self, request, queryset): - limit = int(request.GET.get('limit', 50)) + limit = int(request.GET.get("limit", 50)) queryset, stoken_rev = self.filter_by_stoken(request, queryset) - result = list(queryset[:limit + 1]) + result = list(queryset[: limit + 1]) if len(result) < limit + 1: done = True else: @@ -165,21 +165,21 @@ class BaseViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(queryset, many=True) ret = { - 'data': serializer.data, - 'done': True, # we always return all the items, so it's always done + "data": serializer.data, + "done": True, # we always return all the items, so it's always done } return Response(ret) class CollectionViewSet(BaseViewSet): - allowed_methods = ['GET', 'POST'] - permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdminOrReadOnly, ) + allowed_methods = ["GET", "POST"] + permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdminOrReadOnly,) queryset = Collection.objects.all() serializer_class = CollectionSerializer - lookup_field = 'main_item__uid' - lookup_url_kwarg = 'uid' - stoken_id_fields = ['items__revisions__stoken__id', 'members__stoken__id'] + lookup_field = "main_item__uid" + lookup_url_kwarg = "uid" + stoken_id_fields = ["items__revisions__stoken__id", "members__stoken__id"] def get_queryset(self, queryset=None): if queryset is None: @@ -188,8 +188,8 @@ class CollectionViewSet(BaseViewSet): def get_serializer_context(self): context = super().get_serializer_context() - prefetch = self.request.query_params.get('prefetch', 'auto') - context.update({'request': self.request, 'prefetch': prefetch}) + prefetch = self.request.query_params.get("prefetch", "auto") + context.update({"request": self.request, "prefetch": prefetch}) return context def destroy(self, request, uid=None, *args, **kwargs): @@ -213,17 +213,18 @@ class CollectionViewSet(BaseViewSet): queryset = self.get_queryset() return self.list_common(request, queryset, *args, **kwargs) - @action_decorator(detail=False, methods=['POST']) + @action_decorator(detail=False, methods=["POST"]) def list_multi(self, request, *args, **kwargs): serializer = CollectionListMultiSerializer(data=request.data) serializer.is_valid(raise_exception=True) - collection_types = serializer.validated_data['collectionTypes'] + collection_types = serializer.validated_data["collectionTypes"] queryset = self.get_queryset() # FIXME: Remove the isnull part once we attach collection types to all objects ("collection-type-migration") queryset = queryset.filter( - Q(members__collectionType__uid__in=collection_types) | Q(members__collectionType__isnull=True)) + Q(members__collectionType__uid__in=collection_types) | Q(members__collectionType__isnull=True) + ) return self.list_common(request, queryset, *args, **kwargs) @@ -234,51 +235,50 @@ class CollectionViewSet(BaseViewSet): serializer = self.get_serializer(result, many=True) ret = { - 'data': serializer.data, - 'stoken': new_stoken, - 'done': done, + "data": serializer.data, + "stoken": new_stoken, + "done": done, } stoken_obj = self.get_stoken_obj(request) if stoken_obj is not None: # FIXME: honour limit? (the limit should be combined for data and this because of stoken) remed_qs = CollectionMemberRemoved.objects.filter(user=request.user, stoken__id__gt=stoken_obj.id) - if not ret['done']: + if not ret["done"]: # We only filter by the new_stoken if we are not done. This is because if we are done, the new stoken # can point to the most recent collection change rather than most recent removed membership. remed_qs = remed_qs.filter(stoken__id__lte=new_stoken_obj.id) - remed = remed_qs.values_list('collection__main_item__uid', flat=True) + remed = remed_qs.values_list("collection__main_item__uid", flat=True) if len(remed) > 0: - ret['removedMemberships'] = [{'uid': x} for x in remed] + ret["removedMemberships"] = [{"uid": x} for x in remed] return Response(ret) class CollectionItemViewSet(BaseViewSet): - allowed_methods = ['GET', 'POST', 'PUT'] - permission_classes = BaseViewSet.permission_classes + (permissions.HasWriteAccessOrReadOnly, ) + allowed_methods = ["GET", "POST", "PUT"] + permission_classes = BaseViewSet.permission_classes + (permissions.HasWriteAccessOrReadOnly,) queryset = CollectionItem.objects.all() serializer_class = CollectionItemSerializer - lookup_field = 'uid' - stoken_id_fields = ['revisions__stoken__id'] + lookup_field = "uid" + stoken_id_fields = ["revisions__stoken__id"] def get_queryset(self): - collection_uid = self.kwargs['collection_uid'] + collection_uid = self.kwargs["collection_uid"] try: collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid) except Collection.DoesNotExist: raise Http404("Collection does not exist") # XXX Potentially add this for performance: .prefetch_related('revisions__chunks') - queryset = type(self).queryset.filter(collection__pk=collection.pk, - revisions__current=True) + queryset = type(self).queryset.filter(collection__pk=collection.pk, revisions__current=True) return queryset def get_serializer_context(self): context = super().get_serializer_context() - prefetch = self.request.query_params.get('prefetch', 'auto') - context.update({'request': self.request, 'prefetch': prefetch}) + prefetch = self.request.query_params.get("prefetch", "auto") + context.update({"request": self.request, "prefetch": prefetch}) return context def create(self, request, collection_uid=None, *args, **kwargs): @@ -298,7 +298,7 @@ class CollectionItemViewSet(BaseViewSet): def list(self, request, collection_uid=None, *args, **kwargs): queryset = self.get_queryset() - if not self.request.query_params.get('withCollection', False): + if not self.request.query_params.get("withCollection", False): queryset = queryset.filter(parent__isnull=True) result, new_stoken_obj, done = self.filter_by_stoken_and_limit(request, queryset) @@ -307,27 +307,27 @@ class CollectionItemViewSet(BaseViewSet): serializer = self.get_serializer(result, many=True) ret = { - 'data': serializer.data, - 'stoken': new_stoken, - 'done': done, + "data": serializer.data, + "stoken": new_stoken, + "done": done, } return Response(ret) - @action_decorator(detail=True, methods=['GET']) + @action_decorator(detail=True, methods=["GET"]) def revision(self, request, collection_uid=None, uid=None, *args, **kwargs): col = get_object_or_404(self.get_collection_queryset(Collection.objects), main_item__uid=collection_uid) item = get_object_or_404(col.items, uid=uid) - limit = int(request.GET.get('limit', 50)) - iterator = request.GET.get('iterator', None) + limit = int(request.GET.get("limit", 50)) + iterator = request.GET.get("iterator", None) - queryset = item.revisions.order_by('-id') + queryset = item.revisions.order_by("-id") if iterator is not None: iterator = get_object_or_404(queryset, uid=iterator) queryset = queryset.filter(id__lt=iterator.id) - result = list(queryset[:limit + 1]) + result = list(queryset[: limit + 1]) if len(result) < limit + 1: done = True else: @@ -336,17 +336,17 @@ class CollectionItemViewSet(BaseViewSet): serializer = CollectionItemRevisionSerializer(result, context=self.get_serializer_context(), many=True) - iterator = serializer.data[-1]['uid'] if len(result) > 0 else None + iterator = serializer.data[-1]["uid"] if len(result) > 0 else None ret = { - 'data': serializer.data, - 'iterator': iterator, - 'done': done, + "data": serializer.data, + "iterator": iterator, + "done": done, } return Response(ret) # FIXME: rename to something consistent with what the clients have - maybe list_updates? - @action_decorator(detail=False, methods=['POST']) + @action_decorator(detail=False, methods=["POST"]) def fetch_updates(self, request, collection_uid=None, *args, **kwargs): queryset = self.get_queryset() @@ -356,79 +356,76 @@ class CollectionItemViewSet(BaseViewSet): item_limit = 200 if len(serializer.validated_data) > item_limit: - content = {'code': 'too_many_items', - 'detail': 'Request has too many items. Limit: {}'. format(item_limit)} + content = {"code": "too_many_items", "detail": "Request has too many items. Limit: {}".format(item_limit)} return Response(content, status=status.HTTP_400_BAD_REQUEST) queryset, stoken_rev = self.filter_by_stoken(request, queryset) - uids, etags = zip(*[(item['uid'], item.get('etag')) for item in serializer.validated_data]) + uids, etags = zip(*[(item["uid"], item.get("etag")) for item in serializer.validated_data]) revs = CollectionItemRevision.objects.filter(uid__in=etags, current=True) queryset = queryset.filter(uid__in=uids).exclude(revisions__in=revs) new_stoken_obj = self.get_queryset_stoken(queryset) new_stoken = new_stoken_obj and new_stoken_obj.uid - stoken = stoken_rev and getattr(stoken_rev, 'uid', None) + stoken = stoken_rev and getattr(stoken_rev, "uid", None) new_stoken = new_stoken or stoken serializer = self.get_serializer(queryset, many=True) ret = { - 'data': serializer.data, - 'stoken': new_stoken, - 'done': True, # we always return all the items, so it's always done + "data": serializer.data, + "stoken": new_stoken, + "done": True, # we always return all the items, so it's always done } return Response(ret) - @action_decorator(detail=False, methods=['POST']) + @action_decorator(detail=False, methods=["POST"]) def batch(self, request, collection_uid=None, *args, **kwargs): return self.transaction(request, collection_uid, validate_etag=False) - @action_decorator(detail=False, methods=['POST']) + @action_decorator(detail=False, methods=["POST"]) def transaction(self, request, collection_uid=None, validate_etag=True, *args, **kwargs): - stoken = request.GET.get('stoken', None) + stoken = request.GET.get("stoken", None) with transaction.atomic(): # We need this for locking on the collection object collection_object = get_object_or_404( self.get_collection_queryset(Collection.objects).select_for_update(), # Lock writes on the collection - main_item__uid=collection_uid) + main_item__uid=collection_uid, + ) if stoken is not None and stoken != collection_object.stoken: - content = {'code': 'stale_stoken', 'detail': 'Stoken is too old'} + content = {"code": "stale_stoken", "detail": "Stoken is too old"} return Response(content, status=status.HTTP_409_CONFLICT) - items = request.data.get('items') - deps = request.data.get('deps', None) + items = request.data.get("items") + deps = request.data.get("deps", None) # FIXME: It should just be one serializer context = self.get_serializer_context() - context.update({'validate_etag': validate_etag}) + context.update({"validate_etag": validate_etag}) serializer = self.get_serializer_class()(data=items, context=context, many=True) deps_serializer = CollectionItemDepSerializer(data=deps, context=context, many=True) ser_valid = serializer.is_valid() - deps_ser_valid = (deps is None or deps_serializer.is_valid()) + deps_ser_valid = deps is None or deps_serializer.is_valid() if ser_valid and deps_ser_valid: items = serializer.save(collection=collection_object) - ret = { - } + ret = {} return Response(ret, status=status.HTTP_200_OK) return Response( - { - "items": serializer.errors, - "deps": deps_serializer.errors if deps is not None else [], - }, - status=status.HTTP_409_CONFLICT) + {"items": serializer.errors, "deps": deps_serializer.errors if deps is not None else [],}, + status=status.HTTP_409_CONFLICT, + ) class CollectionItemChunkViewSet(viewsets.ViewSet): - allowed_methods = ['GET', 'PUT'] + allowed_methods = ["GET", "PUT"] authentication_classes = BaseViewSet.authentication_classes permission_classes = BaseViewSet.permission_classes renderer_classes = BaseViewSet.renderer_classes - parser_classes = (ChunkUploadParser, ) + parser_classes = (ChunkUploadParser,) serializer_class = CollectionItemChunkSerializer - lookup_field = 'uid' + lookup_field = "uid" def get_serializer_class(self): return self.serializer_class @@ -452,13 +449,12 @@ class CollectionItemChunkViewSet(viewsets.ViewSet): serializer.save(collection=col) except IntegrityError: return Response( - {"code": "chunk_exists", "detail": "Chunk already exists."}, - status=status.HTTP_409_CONFLICT + {"code": "chunk_exists", "detail": "Chunk already exists."}, status=status.HTTP_409_CONFLICT ) return Response({}, status=status.HTTP_201_CREATED) - @action_decorator(detail=True, methods=['GET']) + @action_decorator(detail=True, methods=["GET"]) def download(self, request, collection_uid=None, collection_item_uid=None, uid=None, *args, **kwargs): import os from django.views.static import serve @@ -476,24 +472,24 @@ class CollectionItemChunkViewSet(viewsets.ViewSet): class CollectionMemberViewSet(BaseViewSet): - allowed_methods = ['GET', 'PUT', 'DELETE'] + allowed_methods = ["GET", "PUT", "DELETE"] our_base_permission_classes = BaseViewSet.permission_classes - permission_classes = our_base_permission_classes + (permissions.IsCollectionAdmin, ) + permission_classes = our_base_permission_classes + (permissions.IsCollectionAdmin,) queryset = CollectionMember.objects.all() serializer_class = CollectionMemberSerializer - lookup_field = f'user__{User.USERNAME_FIELD}__iexact' - lookup_url_kwarg = 'username' - stoken_id_fields = ['stoken__id'] + lookup_field = f"user__{User.USERNAME_FIELD}__iexact" + lookup_url_kwarg = "username" + stoken_id_fields = ["stoken__id"] # FIXME: need to make sure that there's always an admin, and maybe also don't let an owner remove adm access # (if we want to transfer, we need to do that specifically) def get_queryset(self, queryset=None): - collection_uid = self.kwargs['collection_uid'] + collection_uid = self.kwargs["collection_uid"] try: collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid) except Collection.DoesNotExist: - raise Http404('Collection does not exist') + raise Http404("Collection does not exist") if queryset is None: queryset = type(self).queryset @@ -502,18 +498,18 @@ class CollectionMemberViewSet(BaseViewSet): # We override this method because we expect the stoken to be called iterator def get_stoken_obj_id(self, request): - return request.GET.get('iterator', None) + return request.GET.get("iterator", None) def list(self, request, collection_uid=None, *args, **kwargs): - queryset = self.get_queryset().order_by('id') + queryset = self.get_queryset().order_by("id") result, new_stoken_obj, done = self.filter_by_stoken_and_limit(request, queryset) new_stoken = new_stoken_obj and new_stoken_obj.uid serializer = self.get_serializer(result, many=True) ret = { - 'data': serializer.data, - 'iterator': new_stoken, # Here we call it an iterator, it's only stoken for collection/items - 'done': done, + "data": serializer.data, + "iterator": new_stoken, # Here we call it an iterator, it's only stoken for collection/items + "done": done, } return Response(ret) @@ -526,9 +522,9 @@ class CollectionMemberViewSet(BaseViewSet): def perform_destroy(self, instance): instance.revoke() - @action_decorator(detail=False, methods=['POST'], permission_classes=our_base_permission_classes) + @action_decorator(detail=False, methods=["POST"], permission_classes=our_base_permission_classes) def leave(self, request, collection_uid=None, *args, **kwargs): - collection_uid = self.kwargs['collection_uid'] + collection_uid = self.kwargs["collection_uid"] col = get_object_or_404(self.get_collection_queryset(Collection.objects), main_item__uid=collection_uid) member = col.members.get(user=request.user) @@ -540,20 +536,20 @@ class CollectionMemberViewSet(BaseViewSet): class InvitationBaseViewSet(BaseViewSet): queryset = CollectionInvitation.objects.all() serializer_class = CollectionInvitationSerializer - lookup_field = 'uid' - lookup_url_kwarg = 'invitation_uid' + lookup_field = "uid" + lookup_url_kwarg = "invitation_uid" def list(self, request, collection_uid=None, *args, **kwargs): - limit = int(request.GET.get('limit', 50)) - iterator = request.GET.get('iterator', None) + limit = int(request.GET.get("limit", 50)) + iterator = request.GET.get("iterator", None) - queryset = self.get_queryset().order_by('id') + queryset = self.get_queryset().order_by("id") if iterator is not None: iterator = get_object_or_404(queryset, uid=iterator) queryset = queryset.filter(id__gt=iterator.id) - result = list(queryset[:limit + 1]) + result = list(queryset[: limit + 1]) if len(result) < limit + 1: done = True else: @@ -562,19 +558,19 @@ class InvitationBaseViewSet(BaseViewSet): serializer = self.get_serializer(result, many=True) - iterator = serializer.data[-1]['uid'] if len(result) > 0 else None + iterator = serializer.data[-1]["uid"] if len(result) > 0 else None ret = { - 'data': serializer.data, - 'iterator': iterator, - 'done': done, + "data": serializer.data, + "iterator": iterator, + "done": done, } return Response(ret) class InvitationOutgoingViewSet(InvitationBaseViewSet): - allowed_methods = ['GET', 'POST', 'PUT', 'DELETE'] + allowed_methods = ["GET", "POST", "PUT", "DELETE"] def get_queryset(self, queryset=None): if queryset is None: @@ -585,28 +581,29 @@ class InvitationOutgoingViewSet(InvitationBaseViewSet): def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - collection_uid = serializer.validated_data.get('collection', {}).get('uid') + collection_uid = serializer.validated_data.get("collection", {}).get("uid") try: collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid) except Collection.DoesNotExist: - raise Http404('Collection does not exist') + raise Http404("Collection does not exist") - if request.user == serializer.validated_data.get('user'): - content = {'code': 'self_invite', 'detail': 'Inviting yourself is invalid'} + if request.user == serializer.validated_data.get("user"): + content = {"code": "self_invite", "detail": "Inviting yourself is invalid"} return Response(content, status=status.HTTP_400_BAD_REQUEST) if not permissions.is_collection_admin(collection, request.user): - raise PermissionDenied({'code': 'admin_access_required', - 'detail': 'User is not an admin of this collection'}) + raise PermissionDenied( + {"code": "admin_access_required", "detail": "User is not an admin of this collection"} + ) serializer.save(collection=collection) return Response({}, status=status.HTTP_201_CREATED) - @action_decorator(detail=False, allowed_methods=['GET'], methods=['GET']) + @action_decorator(detail=False, allowed_methods=["GET"], methods=["GET"]) def fetch_user_profile(self, request, *args, **kwargs): - username = request.GET.get('username') + username = request.GET.get("username") kwargs = {User.USERNAME_FIELD: username.lower()} user = get_object_or_404(get_user_queryset(User.objects.all(), self), **kwargs) user_info = get_object_or_404(UserInfo.objects.all(), owner=user) @@ -615,7 +612,7 @@ class InvitationOutgoingViewSet(InvitationBaseViewSet): class InvitationIncomingViewSet(InvitationBaseViewSet): - allowed_methods = ['GET', 'DELETE'] + allowed_methods = ["GET", "DELETE"] def get_queryset(self, queryset=None): if queryset is None: @@ -623,11 +620,11 @@ class InvitationIncomingViewSet(InvitationBaseViewSet): return queryset.filter(user=self.request.user) - @action_decorator(detail=True, allowed_methods=['POST'], methods=['POST']) + @action_decorator(detail=True, allowed_methods=["POST"], methods=["POST"]) def accept(self, request, invitation_uid=None, *args, **kwargs): invitation = get_object_or_404(self.get_queryset(), uid=invitation_uid) context = self.get_serializer_context() - context.update({'invitation': invitation}) + context.update({"invitation": invitation}) serializer = InvitationAcceptSerializer(data=request.data, context=context) serializer.is_valid(raise_exception=True) @@ -636,36 +633,37 @@ class InvitationIncomingViewSet(InvitationBaseViewSet): class AuthenticationViewSet(viewsets.ViewSet): - allowed_methods = ['POST'] + allowed_methods = ["POST"] authentication_classes = BaseViewSet.authentication_classes renderer_classes = BaseViewSet.renderer_classes parser_classes = BaseViewSet.parser_classes def get_encryption_key(self, salt): key = nacl.hash.blake2b(settings.SECRET_KEY.encode(), encoder=nacl.encoding.RawEncoder) - return nacl.hash.blake2b(b'', key=key, salt=salt[:nacl.hash.BLAKE2B_SALTBYTES], person=b'etebase-auth', - encoder=nacl.encoding.RawEncoder) + return nacl.hash.blake2b( + b"", + key=key, + salt=salt[: nacl.hash.BLAKE2B_SALTBYTES], + person=b"etebase-auth", + encoder=nacl.encoding.RawEncoder, + ) def get_queryset(self): return get_user_queryset(User.objects.all(), self) def get_serializer_context(self): - return { - 'request': self.request, - 'format': self.format_kwarg, - 'view': self - } + return {"request": self.request, "format": self.format_kwarg, "view": self} def login_response_data(self, user): return { - 'token': AuthToken.objects.create(user=user).key, - 'user': UserSerializer(user).data, + "token": AuthToken.objects.create(user=user).key, + "user": UserSerializer(user).data, } def list(self, request, *args, **kwargs): return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) - @action_decorator(detail=False, methods=['POST']) + @action_decorator(detail=False, methods=["POST"]) def signup(self, request, *args, **kwargs): serializer = AuthenticationSignupSerializer(data=request.data, context=self.get_serializer_context()) serializer.is_valid(raise_exception=True) @@ -677,23 +675,23 @@ class AuthenticationViewSet(viewsets.ViewSet): return Response(data, status=status.HTTP_201_CREATED) def get_login_user(self, username): - kwargs = {User.USERNAME_FIELD + '__iexact': username.lower()} + kwargs = {User.USERNAME_FIELD + "__iexact": username.lower()} try: user = self.get_queryset().get(**kwargs) - if not hasattr(user, 'userinfo'): - raise AuthenticationFailed({'code': 'user_not_init', 'detail': 'User not properly init'}) + if not hasattr(user, "userinfo"): + raise AuthenticationFailed({"code": "user_not_init", "detail": "User not properly init"}) return user except User.DoesNotExist: - raise AuthenticationFailed({'code': 'user_not_found', 'detail': 'User not found'}) + raise AuthenticationFailed({"code": "user_not_found", "detail": "User not found"}) def validate_login_request(self, request, validated_data, response_raw, signature, expected_action): from datetime import datetime - username = validated_data.get('username') + username = validated_data.get("username") user = self.get_login_user(username) - host = validated_data['host'] - challenge = validated_data['challenge'] - action = validated_data['action'] + host = validated_data["host"] + challenge = validated_data["challenge"] + action = validated_data["action"] salt = bytes(user.userinfo.salt) enc_key = self.get_encryption_key(salt) @@ -702,17 +700,17 @@ class AuthenticationViewSet(viewsets.ViewSet): challenge_data = msgpack_decode(box.decrypt(challenge)) now = int(datetime.now().timestamp()) if action != expected_action: - content = {'code': 'wrong_action', 'detail': 'Expected "{}" but got something else'.format(expected_action)} + content = {"code": "wrong_action", "detail": 'Expected "{}" but got something else'.format(expected_action)} return Response(content, status=status.HTTP_400_BAD_REQUEST) - elif now - challenge_data['timestamp'] > app_settings.CHALLENGE_VALID_SECONDS: - content = {'code': 'challenge_expired', 'detail': 'Login challange has expired'} + elif now - challenge_data["timestamp"] > app_settings.CHALLENGE_VALID_SECONDS: + content = {"code": "challenge_expired", "detail": "Login challange has expired"} return Response(content, status=status.HTTP_400_BAD_REQUEST) - elif challenge_data['userId'] != user.id: - content = {'code': 'wrong_user', 'detail': 'This challenge is for the wrong user'} + elif challenge_data["userId"] != user.id: + content = {"code": "wrong_user", "detail": "This challenge is for the wrong user"} return Response(content, status=status.HTTP_400_BAD_REQUEST) - elif not settings.DEBUG and host.split(':', 1)[0] != request.get_host(): + elif not settings.DEBUG and host.split(":", 1)[0] != request.get_host(): detail = 'Found wrong host name. Got: "{}" expected: "{}"'.format(host, request.get_host()) - content = {'code': 'wrong_host', 'detail': detail} + content = {"code": "wrong_host", "detail": detail} return Response(content, status=status.HTTP_400_BAD_REQUEST) verify_key = nacl.signing.VerifyKey(bytes(user.userinfo.loginPubkey), encoder=nacl.encoding.RawEncoder) @@ -720,22 +718,24 @@ class AuthenticationViewSet(viewsets.ViewSet): try: verify_key.verify(response_raw, signature) except nacl.exceptions.BadSignatureError: - return Response({'code': 'login_bad_signature', 'detail': 'Wrong password for user.'}, - status=status.HTTP_401_UNAUTHORIZED) + return Response( + {"code": "login_bad_signature", "detail": "Wrong password for user."}, + status=status.HTTP_401_UNAUTHORIZED, + ) return None - @action_decorator(detail=False, methods=['GET']) + @action_decorator(detail=False, methods=["GET"]) def is_etebase(self, request, *args, **kwargs): return Response({}, status=status.HTTP_200_OK) - @action_decorator(detail=False, methods=['POST']) + @action_decorator(detail=False, methods=["POST"]) def login_challenge(self, request, *args, **kwargs): from datetime import datetime serializer = AuthenticationLoginChallengeSerializer(data=request.data) serializer.is_valid(raise_exception=True) - username = serializer.validated_data.get('username') + username = serializer.validated_data.get("username") user = self.get_login_user(username) salt = bytes(user.userinfo.salt) @@ -755,25 +755,26 @@ class AuthenticationViewSet(viewsets.ViewSet): } return Response(ret, status=status.HTTP_200_OK) - @action_decorator(detail=False, methods=['POST']) + @action_decorator(detail=False, methods=["POST"]) def login(self, request, *args, **kwargs): outer_serializer = AuthenticationLoginSerializer(data=request.data) outer_serializer.is_valid(raise_exception=True) - response_raw = outer_serializer.validated_data['response'] + response_raw = outer_serializer.validated_data["response"] response = msgpack_decode(response_raw) - signature = outer_serializer.validated_data['signature'] + signature = outer_serializer.validated_data["signature"] - context = {'host': request.get_host()} + context = {"host": request.get_host()} serializer = AuthenticationLoginInnerSerializer(data=response, context=context) serializer.is_valid(raise_exception=True) bad_login_response = self.validate_login_request( - request, serializer.validated_data, response_raw, signature, "login") + request, serializer.validated_data, response_raw, signature, "login" + ) if bad_login_response is not None: return bad_login_response - username = serializer.validated_data.get('username') + username = serializer.validated_data.get("username") user = self.get_login_user(username) data = self.login_response_data(user) @@ -782,27 +783,28 @@ class AuthenticationViewSet(viewsets.ViewSet): return Response(data, status=status.HTTP_200_OK) - @action_decorator(detail=False, methods=['POST'], permission_classes=[IsAuthenticated]) + @action_decorator(detail=False, methods=["POST"], permission_classes=[IsAuthenticated]) def logout(self, request, *args, **kwargs): request.auth.delete() user_logged_out.send(sender=request.user.__class__, request=request, user=request.user) return Response(status=status.HTTP_204_NO_CONTENT) - @action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes) + @action_decorator(detail=False, methods=["POST"], permission_classes=BaseViewSet.permission_classes) def change_password(self, request, *args, **kwargs): outer_serializer = AuthenticationLoginSerializer(data=request.data) outer_serializer.is_valid(raise_exception=True) - response_raw = outer_serializer.validated_data['response'] + response_raw = outer_serializer.validated_data["response"] response = msgpack_decode(response_raw) - signature = outer_serializer.validated_data['signature'] + signature = outer_serializer.validated_data["signature"] - context = {'host': request.get_host()} + context = {"host": request.get_host()} serializer = AuthenticationChangePasswordInnerSerializer(request.user.userinfo, data=response, context=context) serializer.is_valid(raise_exception=True) bad_login_response = self.validate_login_request( - request, serializer.validated_data, response_raw, signature, "changePassword") + request, serializer.validated_data, response_raw, signature, "changePassword" + ) if bad_login_response is not None: return bad_login_response @@ -810,35 +812,32 @@ class AuthenticationViewSet(viewsets.ViewSet): return Response({}, status=status.HTTP_200_OK) - @action_decorator(detail=False, methods=['POST'], permission_classes=[IsAuthenticated]) + @action_decorator(detail=False, methods=["POST"], permission_classes=[IsAuthenticated]) def dashboard_url(self, request, *args, **kwargs): get_dashboard_url = app_settings.DASHBOARD_URL_FUNC if get_dashboard_url is None: - raise EtebaseValidationError('not_supported', 'This server doesn\'t have a user dashboard.', - status_code=status.HTTP_400_BAD_REQUEST) + raise EtebaseValidationError( + "not_supported", "This server doesn't have a user dashboard.", status_code=status.HTTP_400_BAD_REQUEST + ) ret = { - 'url': get_dashboard_url(request, *args, **kwargs), + "url": get_dashboard_url(request, *args, **kwargs), } return Response(ret) class TestAuthenticationViewSet(viewsets.ViewSet): - allowed_methods = ['POST'] + allowed_methods = ["POST"] renderer_classes = BaseViewSet.renderer_classes parser_classes = BaseViewSet.parser_classes def get_serializer_context(self): - return { - 'request': self.request, - 'format': self.format_kwarg, - 'view': self - } + return {"request": self.request, "format": self.format_kwarg, "view": self} def list(self, request, *args, **kwargs): return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) - @action_decorator(detail=False, methods=['POST']) + @action_decorator(detail=False, methods=["POST"]) def reset(self, request, *args, **kwargs): # Only run when in DEBUG mode! It's only used for tests if not settings.DEBUG: @@ -846,13 +845,13 @@ class TestAuthenticationViewSet(viewsets.ViewSet): with transaction.atomic(): user_queryset = get_user_queryset(User.objects.all(), self) - user = get_object_or_404(user_queryset, username=request.data.get('user').get('username')) + user = get_object_or_404(user_queryset, username=request.data.get("user").get("username")) # Only allow test users for extra safety - if not getattr(user, User.USERNAME_FIELD).startswith('test_user'): + if not getattr(user, User.USERNAME_FIELD).startswith("test_user"): return HttpResponseBadRequest("Endpoint not allowed for user.") - if hasattr(user, 'userinfo'): + if hasattr(user, "userinfo"): user.userinfo.delete() serializer = AuthenticationSignupSerializer(data=request.data, context=self.get_serializer_context()) diff --git a/etebase_server/asgi.py b/etebase_server/asgi.py index 44f1c53..0bf63ec 100644 --- a/etebase_server/asgi.py +++ b/etebase_server/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'etebase_server.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "etebase_server.settings") application = get_asgi_application() diff --git a/etebase_server/settings.py b/etebase_server/settings.py index ee98f55..9baf8d3 100644 --- a/etebase_server/settings.py +++ b/etebase_server/settings.py @@ -17,7 +17,7 @@ from .utils import get_secret_from_file # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -AUTH_USER_MODEL = 'myauth.User' +AUTH_USER_MODEL = "myauth.User" # Quick-start development settings - unsuitable for production @@ -37,10 +37,9 @@ ALLOWED_HOSTS = [] # https://docs.djangoproject.com/en/2.0/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.environ.get('ETEBASE_DB_PATH', - os.path.join(BASE_DIR, 'db.sqlite3')), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.environ.get("ETEBASE_DB_PATH", os.path.join(BASE_DIR, "db.sqlite3")), } } @@ -48,78 +47,68 @@ DATABASES = { # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'corsheaders', - 'rest_framework', - 'myauth.apps.MyauthConfig', - 'django_etebase.apps.DjangoEtebaseConfig', - 'django_etebase.token_auth.apps.TokenAuthConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "corsheaders", + "rest_framework", + "myauth.apps.MyauthConfig", + "django_etebase.apps.DjangoEtebaseConfig", + "django_etebase.token_auth.apps.TokenAuthConfig", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'etebase_server.urls' +ROOT_URLCONF = "etebase_server.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - os.path.join(BASE_DIR, 'templates') - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'etebase_server.wsgi.application' +WSGI_APPLICATION = "etebase_server.wsgi.application" # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",}, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, ] # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -133,18 +122,18 @@ CORS_ORIGIN_ALLOW_ALL = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ -STATIC_URL = '/static/' -STATIC_ROOT = os.environ.get('DJANGO_STATIC_ROOT', os.path.join(BASE_DIR, 'static')) +STATIC_URL = "/static/" +STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT", os.path.join(BASE_DIR, "static")) -MEDIA_ROOT = os.environ.get('DJANGO_MEDIA_ROOT', os.path.join(BASE_DIR, 'media')) -MEDIA_URL = '/user-media/' +MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", os.path.join(BASE_DIR, "media")) +MEDIA_URL = "/user-media/" # Define where to find configuration files config_locations = [ - os.environ.get('ETEBASE_EASY_CONFIG_PATH', ''), - 'etebase-server.ini', - '/etc/etebase-server/etebase-server.ini', + os.environ.get("ETEBASE_EASY_CONFIG_PATH", ""), + "etebase-server.ini", + "/etc/etebase-server/etebase-server.ini", ] # Use config file if present @@ -152,27 +141,29 @@ if any(os.path.isfile(x) for x in config_locations): config = configparser.ConfigParser() config.read(config_locations) - section = config['global'] + section = config["global"] - SECRET_FILE = section.get('secret_file', SECRET_FILE) - STATIC_ROOT = section.get('static_root', STATIC_ROOT) - STATIC_URL = section.get('static_url', STATIC_URL) - MEDIA_ROOT = section.get('media_root', MEDIA_ROOT) - MEDIA_URL = section.get('media_url', MEDIA_URL) - LANGUAGE_CODE = section.get('language_code', LANGUAGE_CODE) - TIME_ZONE = section.get('time_zone', TIME_ZONE) - DEBUG = section.getboolean('debug', DEBUG) + SECRET_FILE = section.get("secret_file", SECRET_FILE) + STATIC_ROOT = section.get("static_root", STATIC_ROOT) + STATIC_URL = section.get("static_url", STATIC_URL) + MEDIA_ROOT = section.get("media_root", MEDIA_ROOT) + MEDIA_URL = section.get("media_url", MEDIA_URL) + LANGUAGE_CODE = section.get("language_code", LANGUAGE_CODE) + TIME_ZONE = section.get("time_zone", TIME_ZONE) + DEBUG = section.getboolean("debug", DEBUG) - if 'allowed_hosts' in config: - ALLOWED_HOSTS = [y for x, y in config.items('allowed_hosts')] + if "allowed_hosts" in config: + ALLOWED_HOSTS = [y for x, y in config.items("allowed_hosts")] - if 'database' in config: - DATABASES = { 'default': { x.upper(): y for x, y in config.items('database') } } + if "database" in config: + DATABASES = {"default": {x.upper(): y for x, y in config.items("database")}} -ETEBASE_API_PERMISSIONS = ('rest_framework.permissions.IsAuthenticated', ) -ETEBASE_API_AUTHENTICATORS = ('django_etebase.token_auth.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication') -ETEBASE_CREATE_USER_FUNC = 'django_etebase.utils.create_user_blocked' +ETEBASE_API_PERMISSIONS = ("rest_framework.permissions.IsAuthenticated",) +ETEBASE_API_AUTHENTICATORS = ( + "django_etebase.token_auth.authentication.TokenAuthentication", + "rest_framework.authentication.SessionAuthentication", +) +ETEBASE_CREATE_USER_FUNC = "django_etebase.utils.create_user_blocked" # Make an `etebase_server_settings` module available to override settings. try: @@ -180,5 +171,5 @@ try: except ImportError: pass -if 'SECRET_KEY' not in locals(): +if "SECRET_KEY" not in locals(): SECRET_KEY = get_secret_from_file(SECRET_FILE) diff --git a/etebase_server/urls.py b/etebase_server/urls.py index fddc32f..f285977 100644 --- a/etebase_server/urls.py +++ b/etebase_server/urls.py @@ -5,13 +5,12 @@ from django.urls import path from django.views.generic import TemplateView urlpatterns = [ - url(r'^api/', include('django_etebase.urls')), - url(r'^admin/', admin.site.urls), - - path('', TemplateView.as_view(template_name='success.html')), + url(r"^api/", include("django_etebase.urls")), + url(r"^admin/", admin.site.urls), + path("", TemplateView.as_view(template_name="success.html")), ] if settings.DEBUG: urlpatterns += [ - url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework")), ] diff --git a/etebase_server/utils.py b/etebase_server/utils.py index 21c99f2..64ed657 100644 --- a/etebase_server/utils.py +++ b/etebase_server/utils.py @@ -14,6 +14,7 @@ from django.core.management import utils + def get_secret_from_file(path): try: with open(path, "r") as f: diff --git a/etebase_server/wsgi.py b/etebase_server/wsgi.py index cf449a1..908f88c 100644 --- a/etebase_server/wsgi.py +++ b/etebase_server/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'etebase_server.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "etebase_server.settings") application = get_wsgi_application() diff --git a/manage.py b/manage.py index b793fd2..91277fb 100755 --- a/manage.py +++ b/manage.py @@ -5,7 +5,7 @@ import sys def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'etebase_server.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "etebase_server.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -17,5 +17,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/myauth/admin.py b/myauth/admin.py index 0ecde3f..1f4b767 100644 --- a/myauth/admin.py +++ b/myauth/admin.py @@ -6,11 +6,7 @@ from .forms import AdminUserCreationForm class UserAdmin(DjangoUserAdmin): add_form = AdminUserCreationForm - add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('username', ), - }), - ) + add_fieldsets = ((None, {"classes": ("wide",), "fields": ("username",),}),) + admin.site.register(User, UserAdmin) diff --git a/myauth/apps.py b/myauth/apps.py index 611e83d..96cb29b 100644 --- a/myauth/apps.py +++ b/myauth/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class MyauthConfig(AppConfig): - name = 'myauth' + name = "myauth" diff --git a/myauth/forms.py b/myauth/forms.py index 55f7299..7aacb9b 100644 --- a/myauth/forms.py +++ b/myauth/forms.py @@ -14,12 +14,12 @@ class AdminUserCreationForm(forms.ModelForm): class Meta: model = User fields = ("username",) - field_classes = {'username': UsernameField} + field_classes = {"username": UsernameField} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self._meta.model.USERNAME_FIELD in self.fields: - self.fields[self._meta.model.USERNAME_FIELD].widget.attrs['autofocus'] = True + self.fields[self._meta.model.USERNAME_FIELD].widget.attrs["autofocus"] = True def save(self, commit=True): user = super().save(commit=False) @@ -27,4 +27,3 @@ class AdminUserCreationForm(forms.ModelForm): if commit: user.save() return user - diff --git a/myauth/migrations/0001_initial.py b/myauth/migrations/0001_initial.py index 1f81e95..e6c2cba 100644 --- a/myauth/migrations/0001_initial.py +++ b/myauth/migrations/0001_initial.py @@ -11,34 +11,79 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0011_update_proxy_permissions'), + ("auth", "0011_update_proxy_permissions"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("password", models.CharField(max_length=128, verbose_name="password")), + ("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login")), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={"unique": "A user with that username already exists."}, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name="username", + ), + ), + ("first_name", models.CharField(blank=True, max_length=30, verbose_name="first name")), + ("last_name", models.CharField(blank=True, max_length=150, verbose_name="last name")), + ("email", models.EmailField(blank=True, max_length=254, verbose_name="email address")), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ("date_joined", models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined")), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), ], + options={"verbose_name": "user", "verbose_name_plural": "users", "abstract": False,}, + managers=[("objects", django.contrib.auth.models.UserManager()),], ), ] diff --git a/myauth/migrations/0002_auto_20200515_0801.py b/myauth/migrations/0002_auto_20200515_0801.py index 3ce02b2..068c9ae 100644 --- a/myauth/migrations/0002_auto_20200515_0801.py +++ b/myauth/migrations/0002_auto_20200515_0801.py @@ -7,13 +7,20 @@ import myauth.models class Migration(migrations.Migration): dependencies = [ - ('myauth', '0001_initial'), + ("myauth", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='user', - name='username', - field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and ./+/-/_ only.', max_length=150, unique=True, validators=[myauth.models.UnicodeUsernameValidator()], verbose_name='username'), + model_name="user", + name="username", + field=models.CharField( + error_messages={"unique": "A user with that username already exists."}, + help_text="Required. 150 characters or fewer. Letters, digits and ./+/-/_ only.", + max_length=150, + unique=True, + validators=[myauth.models.UnicodeUsernameValidator()], + verbose_name="username", + ), ), ] diff --git a/myauth/models.py b/myauth/models.py index 611555b..d6585a8 100644 --- a/myauth/models.py +++ b/myauth/models.py @@ -7,17 +7,14 @@ from django.utils.translation import gettext_lazy as _ @deconstructible class UnicodeUsernameValidator(validators.RegexValidator): - regex = r'^[\w.-]+\Z' - message = _( - 'Enter a valid username. This value may contain only letters, ' - 'numbers, and ./-/_ characters.' - ) + regex = r"^[\w.-]+\Z" + message = _("Enter a valid username. This value may contain only letters, " "numbers, and ./-/_ characters.") flags = 0 class UserManager(DjangoUserManager): def get_by_natural_key(self, username): - return self.get(**{self.model.USERNAME_FIELD + '__iexact': username}) + return self.get(**{self.model.USERNAME_FIELD + "__iexact": username}) class User(AbstractUser): @@ -26,14 +23,12 @@ class User(AbstractUser): objects = UserManager() username = models.CharField( - _('username'), + _("username"), max_length=150, unique=True, - help_text=_('Required. 150 characters or fewer. Letters, digits and ./-/_ only.'), + help_text=_("Required. 150 characters or fewer. Letters, digits and ./-/_ only."), validators=[username_validator], - error_messages={ - 'unique': _("A user with that username already exists."), - }, + error_messages={"unique": _("A user with that username already exists."),}, ) @classmethod diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e34796e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 120 \ No newline at end of file diff --git a/requirements.in/development.txt b/requirements.in/development.txt index c752bfb..a956471 100644 --- a/requirements.in/development.txt +++ b/requirements.in/development.txt @@ -1,3 +1,4 @@ coverage pip-tools pywatchman +black \ No newline at end of file From b6919d17beb92ce019ca6a9f223282307f3df468 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 14 Nov 2020 15:16:13 +0000 Subject: [PATCH 7/7] chore: fix broken links in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ebb384e..dc05b31 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,9 @@ pip install -r requirements.txt # Configuration -If you are familiar with Django you can just edit the [settings file](etesync_server/settings.py) +If you are familiar with Django you can just edit the [settings file](etebase_server/settings.py) according to the [Django deployment checklist](https://docs.djangoproject.com/en/dev/howto/deployment/checklist/). -If you are not, we also provide a simple [configuration file](https://github.com/etesync/server/blob/etebase/etebase-server.ini.example) for easy deployment which you can use. +If you are not, we also provide a simple [configuration file](etebase-server.ini.example) for easy deployment which you can use. To use the easy configuration file rename it to `etebase-server.ini` and place it either at the root of this repository or in `/etc/etebase-server`. There is also a [wikipage](https://github.com/etesync/server/wiki/Basic-Setup-Etebase-(EteSync-v2)) detailing this basic setup.