Merge remote-tracking branch 'origin/master' into feat/ldap

This commit is contained in:
PapaTutuWawa 2020-11-14 16:39:46 +01:00
commit 6751502e21
67 changed files with 1159 additions and 825 deletions

View File

@ -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.

View File

@ -3,7 +3,9 @@
<h1 align="center">Etebase - Encrypt Everything</h1>
</p>
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
@ -32,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.
@ -84,10 +86,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.

View File

@ -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_")

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class DjangoEtebaseConfig(AppConfig):
name = 'django_etebase'
name = "django_etebase"

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class DrfMsgpackConfig(AppConfig):
name = 'drf_msgpack'
name = "drf_msgpack"

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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")},},
),
]

View File

@ -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)),
],
),
]

View File

@ -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")},},
),
]

View File

@ -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),
),
]

View File

@ -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",),
]

View File

@ -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,
),
]

View File

@ -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}$"
)
],
),
),
]

View File

@ -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"
),
),
]

View File

@ -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 = [

View File

@ -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"),
),
]

View File

@ -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"
),
),
]

View File

@ -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 = [

View File

@ -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")},},
),
]

View File

@ -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",),
]

View File

@ -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),
),
]

View File

@ -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",),
]

View File

@ -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]*$")],
),
),
]

View File

@ -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\\-_]*$")
],
),
),
]

View File

@ -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\\-_]*$")
],
),
),
]

View File

@ -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",),
]

View File

@ -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,}$")
],
),
),
]

View File

@ -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")},),
]

View File

@ -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",
),
),
]

View File

@ -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 = [

View File

@ -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",),
]

View File

@ -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",),
]

View File

@ -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),
),
]

View File

@ -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 = [

View File

@ -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",),
]

View File

@ -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",
),
),
]

View File

@ -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"
),
),
]

View File

@ -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),
),
]

View File

@ -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):

View File

@ -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]

View File

@ -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:

View File

@ -15,4 +15,5 @@ class JSONRenderer(DRFJSONRenderer):
"""
Renderer which serializes to JSON with support for our base64
"""
encoder_class = JSONEncoder

View File

@ -29,7 +29,11 @@ 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
for chunk in chunks:
uid = chunk[0]
chunk_obj = models.CollectionItemChunk.objects.filter(uid=uid).first()
@ -38,24 +42,25 @@ 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)
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
def b64encode(value):
return base64.urlsafe_b64encode(value).decode('ascii').strip('=')
return base64.urlsafe_b64encode(value).decode("ascii").strip("=")
def b64decode(data):
@ -80,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
@ -88,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
@ -97,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):
@ -110,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]))
@ -128,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):
@ -150,52 +149,51 @@ 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
}
class CollectionItemSerializer(BetterErrorsMixin, serializers.ModelSerializer):
@ -205,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
@ -220,9 +218,16 @@ 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)
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
@ -231,7 +236,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
@ -245,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
@ -262,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)
@ -318,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
@ -333,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()
@ -349,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
@ -359,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)
@ -391,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
@ -409,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)
@ -423,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()
@ -437,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):
@ -450,7 +461,7 @@ class UserInfoPubkeySerializer(BetterErrorsMixin, serializers.ModelSerializer):
class Meta:
model = models.UserInfo
fields = ('pubkey', )
fields = ("pubkey",)
class UserSignupSerializer(BetterErrorsMixin, serializers.ModelSerializer):
@ -458,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
}
@ -466,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()
@ -474,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)
@ -543,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

View File

@ -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"])

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class TokenAuthConfig(AppConfig):
name = 'django_etebase.token_auth'
name = "django_etebase.token_auth"

View File

@ -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",))

View File

@ -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,
),
),
],
),
]

View File

@ -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)

View File

@ -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)),
]

View File

@ -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.")

View File

@ -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())

View File

@ -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()

View File

@ -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,11 +122,11 @@ 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/"
ETEBASE_API_PERMISSIONS = ['rest_framework.permissions.IsAuthenticated']
ETEBASE_API_AUTHENTICATORS = ('django_etebase.token_auth.authentication.TokenAuthentication',
@ -145,28 +134,40 @@ ETEBASE_API_AUTHENTICATORS = ('django_etebase.token_auth.authentication.TokenAut
ETEBASE_CREATE_USER_FUNC = 'django_etebase.utils.create_user_blocked'
# 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",
]
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"
# Use config file if present
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")}}
if 'ldap' in config:
ldap = config['ldap']
@ -186,5 +187,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)

View File

@ -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")),
]

View File

@ -14,6 +14,7 @@
from django.core.management import utils
def get_secret_from_file(path):
try:
with open(path, "r") as f:

View File

@ -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()

View File

@ -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()

View File

@ -1,5 +1,12 @@
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)

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class MyauthConfig(AppConfig):
name = 'myauth'
name = "myauth"

29
myauth/forms.py Normal file
View File

@ -0,0 +1,29 @@
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

View File

@ -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()),],
),
]

View File

@ -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",
),
),
]

View File

@ -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

2
pyproject.toml Normal file
View File

@ -0,0 +1,2 @@
[tool.black]
line-length = 120

View File

@ -1,3 +1,4 @@
coverage
pip-tools
pywatchman
black