From 49b31f977760a32777e7b275da0caa6bd61b060f Mon Sep 17 00:00:00 2001 From: Guillaume Vincent Date: Sun, 30 Dec 2018 14:32:07 +0100 Subject: [PATCH] LessPass backend container work --- containers/.env | 7 + containers/backend/.gitignore | 1 + containers/backend/Dockerfile | 26 +++ containers/backend/api/__init__.py | 0 containers/backend/api/admin.py | 84 +++++++++ containers/backend/api/apps.py | 5 + containers/backend/api/migrations/0001_initial.py | 73 ++++++++ containers/backend/api/migrations/0002_password.py | 38 ++++ .../api/migrations/0003_mv_entries_to_password.py | 37 ++++ .../0004_remove_entries_password_info_models.py | 29 ++++ .../api/migrations/0005_password_version.py | 20 +++ .../0006_change_default_password_profile.py | 25 +++ containers/backend/api/migrations/__init__.py | 0 containers/backend/api/models.py | 90 ++++++++++ containers/backend/api/permissions.py | 6 + containers/backend/api/serializers.py | 14 ++ containers/backend/api/tests/__init__.py | 0 containers/backend/api/tests/factories.py | 25 +++ containers/backend/api/tests/tests_passwords.py | 104 +++++++++++ containers/backend/api/urls.py | 15 ++ containers/backend/api/views.py | 14 ++ containers/backend/entrypoint.sh | 7 + containers/backend/lesspass/__init__.py | 0 containers/backend/lesspass/settings.py | 193 +++++++++++++++++++++ containers/backend/lesspass/urls.py | 7 + containers/backend/lesspass/wsgi.py | 7 + containers/backend/manage.py | 10 ++ containers/backend/requirements.txt | 10 ++ containers/backend/wait_db.py | 19 ++ containers/database/.travis.yml | 13 -- containers/database/Dockerfile | 19 -- containers/database/README.md | 24 --- containers/database/api/__init__.py | 0 containers/database/api/admin.py | 84 --------- containers/database/api/apps.py | 5 - containers/database/api/migrations/0001_initial.py | 73 -------- .../database/api/migrations/0002_password.py | 38 ---- .../api/migrations/0003_mv_entries_to_password.py | 37 ---- .../0004_remove_entries_password_info_models.py | 29 ---- .../api/migrations/0005_password_version.py | 20 --- .../0006_change_default_password_profile.py | 25 --- containers/database/api/migrations/__init__.py | 0 containers/database/api/models.py | 90 ---------- containers/database/api/permissions.py | 6 - containers/database/api/serializers.py | 14 -- containers/database/api/tests/__init__.py | 0 containers/database/api/tests/factories.py | 25 --- containers/database/api/tests/tests_passwords.py | 104 ----------- containers/database/api/urls.py | 15 -- containers/database/api/views.py | 14 -- containers/database/entrypoint.sh | 8 - containers/database/lesspass/__init__.py | 0 containers/database/lesspass/settings.py | 193 --------------------- containers/database/lesspass/urls.py | 7 - containers/database/lesspass/wsgi.py | 7 - containers/database/manage.py | 10 -- containers/database/requirements.txt | 10 -- containers/database/supervisord.conf | 14 -- containers/docker-compose.yml | 20 ++- containers/webserver/.gitignore | 1 + containers/webserver/generate_apache_conf.py | 1 - containers/webserver/lesspass.conf.j2 | 9 + 62 files changed, 894 insertions(+), 887 deletions(-) create mode 100644 containers/.env create mode 100644 containers/backend/.gitignore create mode 100644 containers/backend/Dockerfile create mode 100644 containers/backend/api/__init__.py create mode 100644 containers/backend/api/admin.py create mode 100644 containers/backend/api/apps.py create mode 100644 containers/backend/api/migrations/0001_initial.py create mode 100644 containers/backend/api/migrations/0002_password.py create mode 100644 containers/backend/api/migrations/0003_mv_entries_to_password.py create mode 100644 containers/backend/api/migrations/0004_remove_entries_password_info_models.py create mode 100644 containers/backend/api/migrations/0005_password_version.py create mode 100644 containers/backend/api/migrations/0006_change_default_password_profile.py create mode 100644 containers/backend/api/migrations/__init__.py create mode 100644 containers/backend/api/models.py create mode 100644 containers/backend/api/permissions.py create mode 100644 containers/backend/api/serializers.py create mode 100644 containers/backend/api/tests/__init__.py create mode 100644 containers/backend/api/tests/factories.py create mode 100644 containers/backend/api/tests/tests_passwords.py create mode 100644 containers/backend/api/urls.py create mode 100644 containers/backend/api/views.py create mode 100755 containers/backend/entrypoint.sh create mode 100644 containers/backend/lesspass/__init__.py create mode 100644 containers/backend/lesspass/settings.py create mode 100644 containers/backend/lesspass/urls.py create mode 100644 containers/backend/lesspass/wsgi.py create mode 100755 containers/backend/manage.py create mode 100644 containers/backend/requirements.txt create mode 100644 containers/backend/wait_db.py delete mode 100644 containers/database/.travis.yml delete mode 100644 containers/database/Dockerfile delete mode 100644 containers/database/README.md delete mode 100644 containers/database/api/__init__.py delete mode 100644 containers/database/api/admin.py delete mode 100644 containers/database/api/apps.py delete mode 100644 containers/database/api/migrations/0001_initial.py delete mode 100644 containers/database/api/migrations/0002_password.py delete mode 100644 containers/database/api/migrations/0003_mv_entries_to_password.py delete mode 100644 containers/database/api/migrations/0004_remove_entries_password_info_models.py delete mode 100644 containers/database/api/migrations/0005_password_version.py delete mode 100644 containers/database/api/migrations/0006_change_default_password_profile.py delete mode 100644 containers/database/api/migrations/__init__.py delete mode 100644 containers/database/api/models.py delete mode 100644 containers/database/api/permissions.py delete mode 100644 containers/database/api/serializers.py delete mode 100644 containers/database/api/tests/__init__.py delete mode 100644 containers/database/api/tests/factories.py delete mode 100644 containers/database/api/tests/tests_passwords.py delete mode 100644 containers/database/api/urls.py delete mode 100644 containers/database/api/views.py delete mode 100755 containers/database/entrypoint.sh delete mode 100644 containers/database/lesspass/__init__.py delete mode 100644 containers/database/lesspass/settings.py delete mode 100644 containers/database/lesspass/urls.py delete mode 100644 containers/database/lesspass/wsgi.py delete mode 100755 containers/database/manage.py delete mode 100644 containers/database/requirements.txt delete mode 100755 containers/database/supervisord.conf create mode 100644 containers/webserver/.gitignore diff --git a/containers/.env b/containers/.env new file mode 100644 index 0000000..b5f57af --- /dev/null +++ b/containers/.env @@ -0,0 +1,7 @@ +SECRET_KEY=azertyuiopqsdfghjklmwxcvbn123456 +DATABASE_ENGINE=django.db.backends.postgresql +DATABASE_NAME=postgres +DATABASE_USER=postgres +DATABASE_PASSWORD= +DATABASE_HOST=db +DATABASE_PORT=5432 \ No newline at end of file diff --git a/containers/backend/.gitignore b/containers/backend/.gitignore new file mode 100644 index 0000000..baf12b4 --- /dev/null +++ b/containers/backend/.gitignore @@ -0,0 +1 @@ +www \ No newline at end of file diff --git a/containers/backend/Dockerfile b/containers/backend/Dockerfile new file mode 100644 index 0000000..ace1c85 --- /dev/null +++ b/containers/backend/Dockerfile @@ -0,0 +1,26 @@ +FROM centos:7 + +LABEL name="LessPass Web Server" +LABEL maintainer="LessPass " + +ENV LANG en_US.UTF-8 + +RUN yum -y install epel-release && \ + yum -y install python34 python34-pip && \ + yum clean all + +RUN mkdir /backend +WORKDIR /backend +COPY requirements.txt /backend/ +RUN python3 -m pip install --upgrade pip +RUN python3 -m pip install -r requirements.txt + +COPY api/ /backend/api/ +COPY lesspass/ /backend/lesspass/ +COPY manage.py /backend/manage.py +COPY wait_db.py /backend/wait_db.py + +COPY entrypoint.sh / +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["gunicorn", "lesspass.wsgi:application", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "debug", "--bind", "0.0.0.0:8000"] \ No newline at end of file diff --git a/containers/backend/api/__init__.py b/containers/backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/containers/backend/api/admin.py b/containers/backend/api/admin.py new file mode 100644 index 0000000..e10a793 --- /dev/null +++ b/containers/backend/api/admin.py @@ -0,0 +1,84 @@ +from api import models +from api.models import LessPassUser + +from django import forms +from django.contrib import admin +from django.contrib.auth.models import Group +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.forms import ReadOnlyPasswordHashField +from django.db.models import Count + + +class UserCreationForm(forms.ModelForm): + password1 = forms.CharField(label='Password', widget=forms.PasswordInput) + password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) + + class Meta: + model = LessPassUser + fields = ('email',) + + def clean_password2(self): + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError("Passwords don't match") + return password2 + + def save(self, commit=True): + user = super(UserCreationForm, self).save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + return user + + +class UserChangeForm(forms.ModelForm): + password = ReadOnlyPasswordHashField() + + class Meta: + model = LessPassUser + fields = ('email', 'password', 'is_active', 'is_admin') + + def clean_password(self): + return self.initial["password"] + + +class LessPassUserAdmin(BaseUserAdmin): + form = UserChangeForm + add_form = UserCreationForm + + list_display = ('email', 'is_admin', 'column_passwords_count') + list_filter = ('is_admin', 'is_active') + fieldsets = ( + (None, {'fields': ('email', 'password')}), + ('Permissions', {'fields': ('is_admin',)}), + ) + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('email', 'password1', 'password2')} + ), + ) + search_fields = ('email',) + ordering = ('email',) + filter_horizontal = () + + def get_queryset(self, request): + return models.LessPassUser.objects.annotate(passwords_count=Count('passwords')) + + def column_passwords_count(self, instance): + return instance.passwords_count + + column_passwords_count.short_description = 'Password count' + column_passwords_count.admin_order_field = 'passwords_count' + + +class PasswordAdmin(admin.ModelAdmin): + list_display = ('id', 'user',) + search_fields = ('user__email',) + ordering = ('user',) + + +admin.site.register(models.Password, PasswordAdmin) +admin.site.register(LessPassUser, LessPassUserAdmin) +admin.site.unregister(Group) diff --git a/containers/backend/api/apps.py b/containers/backend/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/containers/backend/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/containers/backend/api/migrations/0001_initial.py b/containers/backend/api/migrations/0001_initial.py new file mode 100644 index 0000000..6fbce26 --- /dev/null +++ b/containers/backend/api/migrations/0001_initial.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-04-06 10:11 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='LessPassUser', + 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')), + ('email', models.EmailField(max_length=255, unique=True, verbose_name='email address')), + ('is_active', models.BooleanField(default=True)), + ('is_admin', models.BooleanField(default=False)), + ], + options={ + 'verbose_name_plural': 'Users', + }, + ), + migrations.CreateModel( + name='Entry', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('login', models.CharField(default='', max_length=255)), + ('site', models.CharField(default='', max_length=255)), + ('title', models.CharField(blank=True, max_length=255, null=True)), + ('username', models.CharField(blank=True, max_length=255, null=True)), + ('email', models.EmailField(blank=True, max_length=254, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('url', models.URLField(blank=True, null=True)), + ], + options={ + 'verbose_name_plural': 'Entries', + }, + ), + migrations.CreateModel( + name='PasswordInfo', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('counter', models.IntegerField(default=1)), + ('settings', models.TextField()), + ('length', models.IntegerField(default=12)), + ], + options={ + 'verbose_name_plural': 'Password info', + }, + ), + migrations.AddField( + model_name='entry', + name='password', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.PasswordInfo'), + ), + migrations.AddField( + model_name='entry', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/containers/backend/api/migrations/0002_password.py b/containers/backend/api/migrations/0002_password.py new file mode 100644 index 0000000..473e34b --- /dev/null +++ b/containers/backend/api/migrations/0002_password.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-09-24 08:11 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Password', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('login', models.CharField(blank=True, max_length=255, null=True)), + ('site', models.CharField(blank=True, max_length=255, null=True)), + ('lowercase', models.BooleanField(default=True)), + ('uppercase', models.BooleanField(default=True)), + ('symbols', models.BooleanField(default=True)), + ('numbers', models.BooleanField(default=True)), + ('counter', models.IntegerField(default=1)), + ('length', models.IntegerField(default=12)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='passwords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/containers/backend/api/migrations/0003_mv_entries_to_password.py b/containers/backend/api/migrations/0003_mv_entries_to_password.py new file mode 100644 index 0000000..270f299 --- /dev/null +++ b/containers/backend/api/migrations/0003_mv_entries_to_password.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-09-24 08:13 +from __future__ import unicode_literals + +from django.db import migrations + +import json + +from api import models + + +def create_password_with(entry): + settings = json.dumps(entry.password.settings) + lowercase = 'lowercase' in settings + uppercase = 'uppercase' in settings + symbols = 'symbols' in settings + numbers = 'numbers' in settings + user = models.LessPassUser.objects.get(id=entry.user.id) + models.Password.objects.create(id=entry.id, site=entry.site, login=entry.login, user=user, + lowercase=lowercase, uppercase=uppercase, symbols=symbols, numbers=numbers, + counter=entry.password.counter, length=entry.password.length) + + +def mv_entries_to_password(apps, schema_editor): + Entry = apps.get_model("api", "Entry") + for entry in Entry.objects.all(): + create_password_with(entry) + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0002_password'), + ] + + operations = [ + migrations.RunPython(mv_entries_to_password), + ] diff --git a/containers/backend/api/migrations/0004_remove_entries_password_info_models.py b/containers/backend/api/migrations/0004_remove_entries_password_info_models.py new file mode 100644 index 0000000..5fbb071 --- /dev/null +++ b/containers/backend/api/migrations/0004_remove_entries_password_info_models.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2016-10-25 06:33 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0003_mv_entries_to_password'), + ] + + operations = [ + migrations.RemoveField( + model_name='entry', + name='password', + ), + migrations.RemoveField( + model_name='entry', + name='user', + ), + migrations.DeleteModel( + name='Entry', + ), + migrations.DeleteModel( + name='PasswordInfo', + ), + ] diff --git a/containers/backend/api/migrations/0005_password_version.py b/containers/backend/api/migrations/0005_password_version.py new file mode 100644 index 0000000..385d709 --- /dev/null +++ b/containers/backend/api/migrations/0005_password_version.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-11-21 13:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_remove_entries_password_info_models'), + ] + + operations = [ + migrations.AddField( + model_name='password', + name='version', + field=models.IntegerField(default=1), + ), + ] diff --git a/containers/backend/api/migrations/0006_change_default_password_profile.py b/containers/backend/api/migrations/0006_change_default_password_profile.py new file mode 100644 index 0000000..dc62ee2 --- /dev/null +++ b/containers/backend/api/migrations/0006_change_default_password_profile.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-19 18:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0005_password_version'), + ] + + operations = [ + migrations.AlterField( + model_name='password', + name='length', + field=models.IntegerField(default=16), + ), + migrations.AlterField( + model_name='password', + name='version', + field=models.IntegerField(default=2), + ), + ] diff --git a/containers/backend/api/migrations/__init__.py b/containers/backend/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/containers/backend/api/models.py b/containers/backend/api/models.py new file mode 100644 index 0000000..e0604d2 --- /dev/null +++ b/containers/backend/api/models.py @@ -0,0 +1,90 @@ +import uuid +from django.db import models +from django.contrib.auth.models import ( + BaseUserManager, AbstractBaseUser +) + + +class LesspassUserManager(BaseUserManager): + def create_user(self, email, password=None): + if not email: + raise ValueError('Users must have an email address') + + user = self.model( + email=self.normalize_email(email), + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, password): + user = self.create_user(email, password=password, ) + user.is_admin = True + user.save(using=self._db) + return user + + +class LessPassUser(AbstractBaseUser): + email = models.EmailField(verbose_name='email address', max_length=255, unique=True) + is_active = models.BooleanField(default=True) + is_admin = models.BooleanField(default=False) + + objects = LesspassUserManager() + + USERNAME_FIELD = 'email' + + def get_full_name(self): + return self.email + + def get_short_name(self): + return self.email + + def __str__(self): + return self.email + + def has_perm(self, perm, obj=None): + return True + + def has_module_perms(self, app_label): + return True + + @property + def is_superuser(self): + return self.is_admin + + @property + def is_staff(self): + return self.is_admin + + class Meta: + verbose_name_plural = "Users" + + +class DateMixin(models.Model): + created = models.DateTimeField(auto_now_add=True, verbose_name='created') + modified = models.DateTimeField(auto_now=True, verbose_name='modified') + + class Meta: + abstract = True + + +class Password(DateMixin): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey(LessPassUser, on_delete=models.CASCADE, related_name='passwords') + login = models.CharField(max_length=255, null=True, blank=True) + site = models.CharField(max_length=255, null=True, blank=True) + + lowercase = models.BooleanField(default=True) + uppercase = models.BooleanField(default=True) + symbols = models.BooleanField(default=True) + numbers = models.BooleanField(default=True) + + length = models.IntegerField(default=16) + counter = models.IntegerField(default=1) + + version = models.IntegerField(default=2) + + def __str__(self): + return str(self.id) + diff --git a/containers/backend/api/permissions.py b/containers/backend/api/permissions.py new file mode 100644 index 0000000..55155c8 --- /dev/null +++ b/containers/backend/api/permissions.py @@ -0,0 +1,6 @@ +from rest_framework import permissions + + +class IsOwner(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + return obj.user == request.user diff --git a/containers/backend/api/serializers.py b/containers/backend/api/serializers.py new file mode 100644 index 0000000..95570b5 --- /dev/null +++ b/containers/backend/api/serializers.py @@ -0,0 +1,14 @@ +from api import models +from rest_framework import serializers + + +class PasswordSerializer(serializers.ModelSerializer): + class Meta: + model = models.Password + fields = ('id', 'login', 'site', 'lowercase', 'uppercase', 'symbols', 'numbers', 'counter', 'length', + 'version', 'created', 'modified') + read_only_fields = ('created', 'modified') + + def create(self, validated_data): + user = self.context['request'].user + return models.Password.objects.create(user=user, **validated_data) diff --git a/containers/backend/api/tests/__init__.py b/containers/backend/api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/containers/backend/api/tests/factories.py b/containers/backend/api/tests/factories.py new file mode 100644 index 0000000..db2a425 --- /dev/null +++ b/containers/backend/api/tests/factories.py @@ -0,0 +1,25 @@ +import factory + +from api import models + + +class UserFactory(factory.DjangoModelFactory): + class Meta: + model = models.LessPassUser + + email = factory.Sequence(lambda n: 'u{0}@lesspass.com'.format(n)) + password = factory.PostGenerationMethodCall('set_password', 'password') + is_admin = False + + +class AdminFactory(UserFactory): + is_admin = True + + +class PasswordFactory(factory.DjangoModelFactory): + class Meta: + model = models.Password + + user = factory.SubFactory(UserFactory) + login = 'admin@oslab.fr' + site = 'lesspass.com' diff --git a/containers/backend/api/tests/tests_passwords.py b/containers/backend/api/tests/tests_passwords.py new file mode 100644 index 0000000..8e0e133 --- /dev/null +++ b/containers/backend/api/tests/tests_passwords.py @@ -0,0 +1,104 @@ +from rest_framework.test import APITestCase, APIClient + +from api import models +from api.tests import factories + + +class LogoutApiTestCase(APITestCase): + def test_get_passwords_401(self): + response = self.client.get('/api/passwords/') + self.assertEqual(401, response.status_code) + + +class LoginApiTestCase(APITestCase): + def setUp(self): + self.user = factories.UserFactory() + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + def test_get_empty_passwords(self): + request = self.client.get('/api/passwords/') + self.assertEqual(0, len(request.data['results'])) + + def test_retrieve_its_own_passwords(self): + password = factories.PasswordFactory(user=self.user) + request = self.client.get('/api/passwords/') + self.assertEqual(1, len(request.data['results'])) + self.assertEqual(password.site, request.data['results'][0]['site']) + + def test_cant_retrieve_other_passwords(self): + not_my_password = factories.PasswordFactory(user=factories.UserFactory()) + request = self.client.get('/api/passwords/%s/' % not_my_password.id) + self.assertEqual(404, request.status_code) + + def test_delete_its_own_passwords(self): + password = factories.PasswordFactory(user=self.user) + self.assertEqual(1, models.Password.objects.all().count()) + request = self.client.delete('/api/passwords/%s/' % password.id) + self.assertEqual(204, request.status_code) + self.assertEqual(0, models.Password.objects.all().count()) + + def test_cant_delete_other_password(self): + not_my_password = factories.PasswordFactory(user=factories.UserFactory()) + self.assertEqual(1, models.Password.objects.all().count()) + request = self.client.delete('/api/passwords/%s/' % not_my_password.id) + self.assertEqual(404, request.status_code) + self.assertEqual(1, models.Password.objects.all().count()) + + def test_create_password(self): + password = { + "site": "lesspass.com", + "login": "test@oslab.fr", + "lowercase": True, + "uppercase": True, + "number": True, + "symbol": True, + "counter": 1, + "length": 12 + } + self.assertEqual(0, models.Password.objects.count()) + self.client.post('/api/passwords/', password) + self.assertEqual(1, models.Password.objects.count()) + + def test_create_password_v2(self): + password = { + "site": "lesspass.com", + "login": "test@oslab.fr", + "lowercase": True, + "uppercase": True, + "number": True, + "symbol": True, + "counter": 1, + "length": 12, + "version": 2 + } + self.client.post('/api/passwords/', password) + self.assertEqual(2, models.Password.objects.first().version) + + def test_update_password(self): + password = factories.PasswordFactory(user=self.user) + self.assertNotEqual('facebook.com', password.site) + new_password = { + "site": "facebook.com", + "login": "test@oslab.fr", + "lowercase": True, + "uppercase": True, + "number": True, + "symbol": True, + "counter": 1, + "length": 12 + } + request = self.client.put('/api/passwords/%s/' % password.id, new_password) + self.assertEqual(200, request.status_code, request.content.decode('utf-8')) + password_updated = models.Password.objects.get(id=password.id) + self.assertEqual('facebook.com', password_updated.site) + + def test_cant_update_other_password(self): + not_my_password = factories.PasswordFactory(user=factories.UserFactory()) + self.assertEqual('lesspass.com', not_my_password.site) + new_password = { + "site": "facebook", + } + request = self.client.put('/api/passwords/%s/' % not_my_password.id, new_password) + self.assertEqual(404, request.status_code) + self.assertEqual(1, models.Password.objects.all().count()) diff --git a/containers/backend/api/urls.py b/containers/backend/api/urls.py new file mode 100644 index 0000000..647cd02 --- /dev/null +++ b/containers/backend/api/urls.py @@ -0,0 +1,15 @@ +import rest_framework_jwt.views +from django.conf.urls import url, include +from rest_framework.routers import DefaultRouter + +from api import views + +router = DefaultRouter() +router.register(r'passwords', views.PasswordViewSet, base_name='passwords') + +urlpatterns = [ + url(r'^', include(router.urls)), + url(r'^tokens/auth/', rest_framework_jwt.views.obtain_jwt_token), + url(r'^tokens/refresh/', rest_framework_jwt.views.refresh_jwt_token), + url(r'^auth/', include('djoser.urls')), +] diff --git a/containers/backend/api/views.py b/containers/backend/api/views.py new file mode 100644 index 0000000..eb758b6 --- /dev/null +++ b/containers/backend/api/views.py @@ -0,0 +1,14 @@ +from api import models, serializers +from api.permissions import IsOwner + +from rest_framework import permissions, viewsets + + +class PasswordViewSet(viewsets.ModelViewSet): + serializer_class = serializers.PasswordSerializer + permission_classes = (permissions.IsAuthenticated, IsOwner,) + search_fields = ('site', 'email',) + ordering_fields = ('site', 'email', 'created') + + def get_queryset(self): + return models.Password.objects.filter(user=self.request.user) diff --git a/containers/backend/entrypoint.sh b/containers/backend/entrypoint.sh new file mode 100755 index 0000000..cfe3532 --- /dev/null +++ b/containers/backend/entrypoint.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +python3 manage.py migrate +python3 manage.py collectstatic --clear --no-input +python3 wait_db.py + +exec "$@" \ No newline at end of file diff --git a/containers/backend/lesspass/__init__.py b/containers/backend/lesspass/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/containers/backend/lesspass/settings.py b/containers/backend/lesspass/settings.py new file mode 100644 index 0000000..a2c5e31 --- /dev/null +++ b/containers/backend/lesspass/settings.py @@ -0,0 +1,193 @@ +import logging +import os +import random +import sys +import datetime +from envparse import env + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def get_secret_key(secret_key): + if not secret_key: + return "".join([random.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$^&*(-_=+)") for i in range(50)]) + return secret_key + + +SECRET_KEY = env('SECRET_KEY', preprocessor=get_secret_key, default=None) + +DEBUG = env.bool('DJANGO_DEBUG', default=False) + +ALLOWED_HOSTS = env('ALLOWED_HOSTS', cast=list, default=['backend', '.lesspass.com']) + +ADMINS = (('Guillaume Vincent', 'guillaume@oslab.fr'),) + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + 'djoser', + 'api' +] + +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', +] + +CORS_ORIGIN_ALLOW_ALL = True + +ROOT_URLCONF = 'lesspass.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + '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 = 'lesspass.wsgi.application' + +DATABASES = { + 'default': { + 'ENGINE': env('DATABASE_ENGINE', default='django.db.backends.sqlite3'), + 'NAME': env('DATABASE_NAME', default=os.path.join(BASE_DIR, 'db.sqlite3')), + 'USER': env('DATABASE_USER', default=None), + 'PASSWORD': env('DATABASE_PASSWORD', default=None), + 'HOST': env('DATABASE_HOST', default=None), + 'PORT': env('DATABASE_PORT', default=None), + } +} + +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', }, +] + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +STATIC_URL = '/static/' + +STATIC_ROOT = os.path.join(BASE_DIR, 'www', 'static') + +MEDIA_URL = '/media/' + +MEDIA_ROOT = os.path.join(BASE_DIR, 'www', 'media') + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + } + }, + 'root': { + 'handlers': ['console'], + 'level': env('DJANGO_LOG_LEVEL', default='DEBUG'), + } +} + +AUTH_USER_MODEL = 'api.LessPassUser' + +JWT_AUTH = { + 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), + 'JWT_ALLOW_REFRESH': True, +} + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': 1000, + 'DEFAULT_FILTER_BACKENDS': ( + 'rest_framework.filters.OrderingFilter', + 'rest_framework.filters.SearchFilter', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + 'DEFAULT_RENDERER_CLASSES': ( + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ), + 'DEFAULT_PARSER_CLASSES': ( + 'rest_framework.parsers.JSONParser', + ), + 'TEST_REQUEST_DEFAULT_FORMAT': 'json' +} + + +class DisableMigrations(object): + def __contains__(self, item): + return True + + def __getitem__(self, item): + return None + + +TESTS_IN_PROGRESS = False +if 'test' in sys.argv[1:] or 'jenkins' in sys.argv[1:]: + logging.disable(logging.CRITICAL) + PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.MD5PasswordHasher', + ) + DEBUG = False + TEMPLATE_DEBUG = False + TESTS_IN_PROGRESS = True + MIGRATION_MODULES = DisableMigrations() + +DJOSER = { + 'PASSWORD_RESET_CONFIRM_URL': '#/password/reset/confirm/{uid}/{token}', + 'ACTIVATION_URL': '#/activate/{uid}/{token}' +} + +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +if DEBUG: + EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend') +else: + EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.smtp.EmailBackend') +DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'contact@lesspass.com') +EMAIL_HOST = os.getenv('EMAIL_HOST', 'localhost') +EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '') +EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '') +EMAIL_PORT = env.int('EMAIL_PORT', default=25) +EMAIL_SUBJECT_PREFIX = os.getenv('EMAIL_SUBJECT_PREFIX', '[LessPass] ') +EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=False) +EMAIL_USE_SSL = env.bool('EMAIL_USE_SSL', default=False) diff --git a/containers/backend/lesspass/urls.py b/containers/backend/lesspass/urls.py new file mode 100644 index 0000000..7ffb69f --- /dev/null +++ b/containers/backend/lesspass/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import include, url +from django.contrib import admin + +urlpatterns = [ + url(r'^api/', include('api.urls')), + url(r'^admin/', admin.site.urls), +] diff --git a/containers/backend/lesspass/wsgi.py b/containers/backend/lesspass/wsgi.py new file mode 100644 index 0000000..bc4d7c4 --- /dev/null +++ b/containers/backend/lesspass/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lesspass.settings") + +application = get_wsgi_application() diff --git a/containers/backend/manage.py b/containers/backend/manage.py new file mode 100755 index 0000000..1571b63 --- /dev/null +++ b/containers/backend/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lesspass.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/containers/backend/requirements.txt b/containers/backend/requirements.txt new file mode 100644 index 0000000..51df6d5 --- /dev/null +++ b/containers/backend/requirements.txt @@ -0,0 +1,10 @@ +Django==1.11 +django-cors-middleware==1.3.1 +djangorestframework==3.6.2 +djangorestframework-jwt==1.10.0 +psycopg2==2.7.1 +gunicorn==19.7.1 +djoser==0.5.4 +envparse==0.2.0 +# tests +factory-boy==2.8.1 diff --git a/containers/backend/wait_db.py b/containers/backend/wait_db.py new file mode 100644 index 0000000..c854ef8 --- /dev/null +++ b/containers/backend/wait_db.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +import socket +import time +import os + +host = os.environ.get('DATABASE_HOST', 'db') +port = int(os.environ.get('DATABASE_PORT', '5432')) + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +timeout = 15 +while timeout != 0: + try: + s.connect((host, port)) + s.close() + break + except socket.error as ex: + timeout -= 1 + print('wait for db to start... (%s sec remaining)' % timeout) +time.sleep(1) \ No newline at end of file diff --git a/containers/database/.travis.yml b/containers/database/.travis.yml deleted file mode 100644 index 17f5242..0000000 --- a/containers/database/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -dist: trusty -sudo: required -language: python -python: - - 3.5 -addons: - postgresql: "9.5" -services: - - postgresql -env: - - DATABASE_ENGINE=django.db.backends.postgresql_psycopg2 DATABASE_NAME=postgres DATABASE_USER=postgres DATABASE_PASSWORD= DATABASE_HOST=localhost DATABASE_PORT=5432 -install: pip install -r requirements.txt -script: python manage.py test diff --git a/containers/database/Dockerfile b/containers/database/Dockerfile deleted file mode 100644 index 7e8fdd4..0000000 --- a/containers/database/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.5-alpine - -RUN apk add --no-cache supervisor netcat-openbsd postgresql-dev gcc python3-dev musl-dev - -RUN mkdir /backend -WORKDIR /backend -COPY requirements.txt /backend/ -RUN pip install --upgrade pip -RUN pip install -r requirements.txt - -COPY api/ /backend/api/ -COPY lesspass/ /backend/lesspass/ -COPY manage.py /backend/manage.py - -COPY entrypoint.sh / -ENTRYPOINT ["/entrypoint.sh"] - -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/containers/database/README.md b/containers/database/README.md deleted file mode 100644 index 965d580..0000000 --- a/containers/database/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# LessPass backend - -REST API used by [lesspass-pure](https://github.com/lesspass/pure) to store password profiles - - * python 3 - * django rest framework - * django rest framework jwt - * djoser - * postgresql - * gunicorn - -## Tests - - pip install -r requirements.txt - python manage.py test - -## License - -This project is licensed under the terms of the GNU GPLv3. - - -## Issues - -report issues on [LessPass project](https://github.com/lesspass/lesspass/issues) diff --git a/containers/database/api/__init__.py b/containers/database/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/containers/database/api/admin.py b/containers/database/api/admin.py deleted file mode 100644 index e10a793..0000000 --- a/containers/database/api/admin.py +++ /dev/null @@ -1,84 +0,0 @@ -from api import models -from api.models import LessPassUser - -from django import forms -from django.contrib import admin -from django.contrib.auth.models import Group -from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from django.contrib.auth.forms import ReadOnlyPasswordHashField -from django.db.models import Count - - -class UserCreationForm(forms.ModelForm): - password1 = forms.CharField(label='Password', widget=forms.PasswordInput) - password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) - - class Meta: - model = LessPassUser - fields = ('email',) - - def clean_password2(self): - password1 = self.cleaned_data.get("password1") - password2 = self.cleaned_data.get("password2") - if password1 and password2 and password1 != password2: - raise forms.ValidationError("Passwords don't match") - return password2 - - def save(self, commit=True): - user = super(UserCreationForm, self).save(commit=False) - user.set_password(self.cleaned_data["password1"]) - if commit: - user.save() - return user - - -class UserChangeForm(forms.ModelForm): - password = ReadOnlyPasswordHashField() - - class Meta: - model = LessPassUser - fields = ('email', 'password', 'is_active', 'is_admin') - - def clean_password(self): - return self.initial["password"] - - -class LessPassUserAdmin(BaseUserAdmin): - form = UserChangeForm - add_form = UserCreationForm - - list_display = ('email', 'is_admin', 'column_passwords_count') - list_filter = ('is_admin', 'is_active') - fieldsets = ( - (None, {'fields': ('email', 'password')}), - ('Permissions', {'fields': ('is_admin',)}), - ) - add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('email', 'password1', 'password2')} - ), - ) - search_fields = ('email',) - ordering = ('email',) - filter_horizontal = () - - def get_queryset(self, request): - return models.LessPassUser.objects.annotate(passwords_count=Count('passwords')) - - def column_passwords_count(self, instance): - return instance.passwords_count - - column_passwords_count.short_description = 'Password count' - column_passwords_count.admin_order_field = 'passwords_count' - - -class PasswordAdmin(admin.ModelAdmin): - list_display = ('id', 'user',) - search_fields = ('user__email',) - ordering = ('user',) - - -admin.site.register(models.Password, PasswordAdmin) -admin.site.register(LessPassUser, LessPassUserAdmin) -admin.site.unregister(Group) diff --git a/containers/database/api/apps.py b/containers/database/api/apps.py deleted file mode 100644 index d87006d..0000000 --- a/containers/database/api/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ApiConfig(AppConfig): - name = 'api' diff --git a/containers/database/api/migrations/0001_initial.py b/containers/database/api/migrations/0001_initial.py deleted file mode 100644 index 6fbce26..0000000 --- a/containers/database/api/migrations/0001_initial.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.5 on 2016-04-06 10:11 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='LessPassUser', - 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')), - ('email', models.EmailField(max_length=255, unique=True, verbose_name='email address')), - ('is_active', models.BooleanField(default=True)), - ('is_admin', models.BooleanField(default=False)), - ], - options={ - 'verbose_name_plural': 'Users', - }, - ), - migrations.CreateModel( - name='Entry', - fields=[ - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('login', models.CharField(default='', max_length=255)), - ('site', models.CharField(default='', max_length=255)), - ('title', models.CharField(blank=True, max_length=255, null=True)), - ('username', models.CharField(blank=True, max_length=255, null=True)), - ('email', models.EmailField(blank=True, max_length=254, null=True)), - ('description', models.TextField(blank=True, null=True)), - ('url', models.URLField(blank=True, null=True)), - ], - options={ - 'verbose_name_plural': 'Entries', - }, - ), - migrations.CreateModel( - name='PasswordInfo', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('counter', models.IntegerField(default=1)), - ('settings', models.TextField()), - ('length', models.IntegerField(default=12)), - ], - options={ - 'verbose_name_plural': 'Password info', - }, - ), - migrations.AddField( - model_name='entry', - name='password', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.PasswordInfo'), - ), - migrations.AddField( - model_name='entry', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/containers/database/api/migrations/0002_password.py b/containers/database/api/migrations/0002_password.py deleted file mode 100644 index 473e34b..0000000 --- a/containers/database/api/migrations/0002_password.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.6 on 2016-09-24 08:11 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Password', - fields=[ - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('login', models.CharField(blank=True, max_length=255, null=True)), - ('site', models.CharField(blank=True, max_length=255, null=True)), - ('lowercase', models.BooleanField(default=True)), - ('uppercase', models.BooleanField(default=True)), - ('symbols', models.BooleanField(default=True)), - ('numbers', models.BooleanField(default=True)), - ('counter', models.IntegerField(default=1)), - ('length', models.IntegerField(default=12)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='passwords', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/containers/database/api/migrations/0003_mv_entries_to_password.py b/containers/database/api/migrations/0003_mv_entries_to_password.py deleted file mode 100644 index 270f299..0000000 --- a/containers/database/api/migrations/0003_mv_entries_to_password.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.6 on 2016-09-24 08:13 -from __future__ import unicode_literals - -from django.db import migrations - -import json - -from api import models - - -def create_password_with(entry): - settings = json.dumps(entry.password.settings) - lowercase = 'lowercase' in settings - uppercase = 'uppercase' in settings - symbols = 'symbols' in settings - numbers = 'numbers' in settings - user = models.LessPassUser.objects.get(id=entry.user.id) - models.Password.objects.create(id=entry.id, site=entry.site, login=entry.login, user=user, - lowercase=lowercase, uppercase=uppercase, symbols=symbols, numbers=numbers, - counter=entry.password.counter, length=entry.password.length) - - -def mv_entries_to_password(apps, schema_editor): - Entry = apps.get_model("api", "Entry") - for entry in Entry.objects.all(): - create_password_with(entry) - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0002_password'), - ] - - operations = [ - migrations.RunPython(mv_entries_to_password), - ] diff --git a/containers/database/api/migrations/0004_remove_entries_password_info_models.py b/containers/database/api/migrations/0004_remove_entries_password_info_models.py deleted file mode 100644 index 5fbb071..0000000 --- a/containers/database/api/migrations/0004_remove_entries_password_info_models.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.1 on 2016-10-25 06:33 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0003_mv_entries_to_password'), - ] - - operations = [ - migrations.RemoveField( - model_name='entry', - name='password', - ), - migrations.RemoveField( - model_name='entry', - name='user', - ), - migrations.DeleteModel( - name='Entry', - ), - migrations.DeleteModel( - name='PasswordInfo', - ), - ] diff --git a/containers/database/api/migrations/0005_password_version.py b/containers/database/api/migrations/0005_password_version.py deleted file mode 100644 index 385d709..0000000 --- a/containers/database/api/migrations/0005_password_version.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.3 on 2016-11-21 13:30 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0004_remove_entries_password_info_models'), - ] - - operations = [ - migrations.AddField( - model_name='password', - name='version', - field=models.IntegerField(default=1), - ), - ] diff --git a/containers/database/api/migrations/0006_change_default_password_profile.py b/containers/database/api/migrations/0006_change_default_password_profile.py deleted file mode 100644 index dc62ee2..0000000 --- a/containers/database/api/migrations/0006_change_default_password_profile.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2017-05-19 18:03 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0005_password_version'), - ] - - operations = [ - migrations.AlterField( - model_name='password', - name='length', - field=models.IntegerField(default=16), - ), - migrations.AlterField( - model_name='password', - name='version', - field=models.IntegerField(default=2), - ), - ] diff --git a/containers/database/api/migrations/__init__.py b/containers/database/api/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/containers/database/api/models.py b/containers/database/api/models.py deleted file mode 100644 index e0604d2..0000000 --- a/containers/database/api/models.py +++ /dev/null @@ -1,90 +0,0 @@ -import uuid -from django.db import models -from django.contrib.auth.models import ( - BaseUserManager, AbstractBaseUser -) - - -class LesspassUserManager(BaseUserManager): - def create_user(self, email, password=None): - if not email: - raise ValueError('Users must have an email address') - - user = self.model( - email=self.normalize_email(email), - ) - - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, email, password): - user = self.create_user(email, password=password, ) - user.is_admin = True - user.save(using=self._db) - return user - - -class LessPassUser(AbstractBaseUser): - email = models.EmailField(verbose_name='email address', max_length=255, unique=True) - is_active = models.BooleanField(default=True) - is_admin = models.BooleanField(default=False) - - objects = LesspassUserManager() - - USERNAME_FIELD = 'email' - - def get_full_name(self): - return self.email - - def get_short_name(self): - return self.email - - def __str__(self): - return self.email - - def has_perm(self, perm, obj=None): - return True - - def has_module_perms(self, app_label): - return True - - @property - def is_superuser(self): - return self.is_admin - - @property - def is_staff(self): - return self.is_admin - - class Meta: - verbose_name_plural = "Users" - - -class DateMixin(models.Model): - created = models.DateTimeField(auto_now_add=True, verbose_name='created') - modified = models.DateTimeField(auto_now=True, verbose_name='modified') - - class Meta: - abstract = True - - -class Password(DateMixin): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - user = models.ForeignKey(LessPassUser, on_delete=models.CASCADE, related_name='passwords') - login = models.CharField(max_length=255, null=True, blank=True) - site = models.CharField(max_length=255, null=True, blank=True) - - lowercase = models.BooleanField(default=True) - uppercase = models.BooleanField(default=True) - symbols = models.BooleanField(default=True) - numbers = models.BooleanField(default=True) - - length = models.IntegerField(default=16) - counter = models.IntegerField(default=1) - - version = models.IntegerField(default=2) - - def __str__(self): - return str(self.id) - diff --git a/containers/database/api/permissions.py b/containers/database/api/permissions.py deleted file mode 100644 index 55155c8..0000000 --- a/containers/database/api/permissions.py +++ /dev/null @@ -1,6 +0,0 @@ -from rest_framework import permissions - - -class IsOwner(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - return obj.user == request.user diff --git a/containers/database/api/serializers.py b/containers/database/api/serializers.py deleted file mode 100644 index 95570b5..0000000 --- a/containers/database/api/serializers.py +++ /dev/null @@ -1,14 +0,0 @@ -from api import models -from rest_framework import serializers - - -class PasswordSerializer(serializers.ModelSerializer): - class Meta: - model = models.Password - fields = ('id', 'login', 'site', 'lowercase', 'uppercase', 'symbols', 'numbers', 'counter', 'length', - 'version', 'created', 'modified') - read_only_fields = ('created', 'modified') - - def create(self, validated_data): - user = self.context['request'].user - return models.Password.objects.create(user=user, **validated_data) diff --git a/containers/database/api/tests/__init__.py b/containers/database/api/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/containers/database/api/tests/factories.py b/containers/database/api/tests/factories.py deleted file mode 100644 index db2a425..0000000 --- a/containers/database/api/tests/factories.py +++ /dev/null @@ -1,25 +0,0 @@ -import factory - -from api import models - - -class UserFactory(factory.DjangoModelFactory): - class Meta: - model = models.LessPassUser - - email = factory.Sequence(lambda n: 'u{0}@lesspass.com'.format(n)) - password = factory.PostGenerationMethodCall('set_password', 'password') - is_admin = False - - -class AdminFactory(UserFactory): - is_admin = True - - -class PasswordFactory(factory.DjangoModelFactory): - class Meta: - model = models.Password - - user = factory.SubFactory(UserFactory) - login = 'admin@oslab.fr' - site = 'lesspass.com' diff --git a/containers/database/api/tests/tests_passwords.py b/containers/database/api/tests/tests_passwords.py deleted file mode 100644 index 8e0e133..0000000 --- a/containers/database/api/tests/tests_passwords.py +++ /dev/null @@ -1,104 +0,0 @@ -from rest_framework.test import APITestCase, APIClient - -from api import models -from api.tests import factories - - -class LogoutApiTestCase(APITestCase): - def test_get_passwords_401(self): - response = self.client.get('/api/passwords/') - self.assertEqual(401, response.status_code) - - -class LoginApiTestCase(APITestCase): - def setUp(self): - self.user = factories.UserFactory() - self.client = APIClient() - self.client.force_authenticate(user=self.user) - - def test_get_empty_passwords(self): - request = self.client.get('/api/passwords/') - self.assertEqual(0, len(request.data['results'])) - - def test_retrieve_its_own_passwords(self): - password = factories.PasswordFactory(user=self.user) - request = self.client.get('/api/passwords/') - self.assertEqual(1, len(request.data['results'])) - self.assertEqual(password.site, request.data['results'][0]['site']) - - def test_cant_retrieve_other_passwords(self): - not_my_password = factories.PasswordFactory(user=factories.UserFactory()) - request = self.client.get('/api/passwords/%s/' % not_my_password.id) - self.assertEqual(404, request.status_code) - - def test_delete_its_own_passwords(self): - password = factories.PasswordFactory(user=self.user) - self.assertEqual(1, models.Password.objects.all().count()) - request = self.client.delete('/api/passwords/%s/' % password.id) - self.assertEqual(204, request.status_code) - self.assertEqual(0, models.Password.objects.all().count()) - - def test_cant_delete_other_password(self): - not_my_password = factories.PasswordFactory(user=factories.UserFactory()) - self.assertEqual(1, models.Password.objects.all().count()) - request = self.client.delete('/api/passwords/%s/' % not_my_password.id) - self.assertEqual(404, request.status_code) - self.assertEqual(1, models.Password.objects.all().count()) - - def test_create_password(self): - password = { - "site": "lesspass.com", - "login": "test@oslab.fr", - "lowercase": True, - "uppercase": True, - "number": True, - "symbol": True, - "counter": 1, - "length": 12 - } - self.assertEqual(0, models.Password.objects.count()) - self.client.post('/api/passwords/', password) - self.assertEqual(1, models.Password.objects.count()) - - def test_create_password_v2(self): - password = { - "site": "lesspass.com", - "login": "test@oslab.fr", - "lowercase": True, - "uppercase": True, - "number": True, - "symbol": True, - "counter": 1, - "length": 12, - "version": 2 - } - self.client.post('/api/passwords/', password) - self.assertEqual(2, models.Password.objects.first().version) - - def test_update_password(self): - password = factories.PasswordFactory(user=self.user) - self.assertNotEqual('facebook.com', password.site) - new_password = { - "site": "facebook.com", - "login": "test@oslab.fr", - "lowercase": True, - "uppercase": True, - "number": True, - "symbol": True, - "counter": 1, - "length": 12 - } - request = self.client.put('/api/passwords/%s/' % password.id, new_password) - self.assertEqual(200, request.status_code, request.content.decode('utf-8')) - password_updated = models.Password.objects.get(id=password.id) - self.assertEqual('facebook.com', password_updated.site) - - def test_cant_update_other_password(self): - not_my_password = factories.PasswordFactory(user=factories.UserFactory()) - self.assertEqual('lesspass.com', not_my_password.site) - new_password = { - "site": "facebook", - } - request = self.client.put('/api/passwords/%s/' % not_my_password.id, new_password) - self.assertEqual(404, request.status_code) - self.assertEqual(1, models.Password.objects.all().count()) diff --git a/containers/database/api/urls.py b/containers/database/api/urls.py deleted file mode 100644 index 647cd02..0000000 --- a/containers/database/api/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -import rest_framework_jwt.views -from django.conf.urls import url, include -from rest_framework.routers import DefaultRouter - -from api import views - -router = DefaultRouter() -router.register(r'passwords', views.PasswordViewSet, base_name='passwords') - -urlpatterns = [ - url(r'^', include(router.urls)), - url(r'^tokens/auth/', rest_framework_jwt.views.obtain_jwt_token), - url(r'^tokens/refresh/', rest_framework_jwt.views.refresh_jwt_token), - url(r'^auth/', include('djoser.urls')), -] diff --git a/containers/database/api/views.py b/containers/database/api/views.py deleted file mode 100644 index eb758b6..0000000 --- a/containers/database/api/views.py +++ /dev/null @@ -1,14 +0,0 @@ -from api import models, serializers -from api.permissions import IsOwner - -from rest_framework import permissions, viewsets - - -class PasswordViewSet(viewsets.ModelViewSet): - serializer_class = serializers.PasswordSerializer - permission_classes = (permissions.IsAuthenticated, IsOwner,) - search_fields = ('site', 'email',) - ordering_fields = ('site', 'email', 'created') - - def get_queryset(self): - return models.Password.objects.filter(user=self.request.user) diff --git a/containers/database/entrypoint.sh b/containers/database/entrypoint.sh deleted file mode 100755 index 832ea14..0000000 --- a/containers/database/entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -while ! nc -z db 5432; do sleep 3; done - -python manage.py migrate -python manage.py collectstatic --clear --no-input - -exec "$@" \ No newline at end of file diff --git a/containers/database/lesspass/__init__.py b/containers/database/lesspass/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/containers/database/lesspass/settings.py b/containers/database/lesspass/settings.py deleted file mode 100644 index 5fbedab..0000000 --- a/containers/database/lesspass/settings.py +++ /dev/null @@ -1,193 +0,0 @@ -import logging -import os -import random -import sys -import datetime -from envparse import env - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -def get_secret_key(secret_key): - if not secret_key: - return "".join([random.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$^&*(-_=+)") for i in range(50)]) - return secret_key - - -SECRET_KEY = env('SECRET_KEY', preprocessor=get_secret_key, default=None) - -DEBUG = env.bool('DJANGO_DEBUG', default=False) - -ALLOWED_HOSTS = env('ALLOWED_HOSTS', cast=list, default=['localhost', '127.0.0.1', '.lesspass.com']) - -ADMINS = (('Guillaume Vincent', 'guillaume@oslab.fr'),) - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'corsheaders', - 'djoser', - 'api' -] - -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', -] - -CORS_ORIGIN_ALLOW_ALL = True - -ROOT_URLCONF = 'lesspass.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - '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 = 'lesspass.wsgi.application' - -DATABASES = { - 'default': { - 'ENGINE': env('DATABASE_ENGINE', default='django.db.backends.sqlite3'), - 'NAME': env('DATABASE_NAME', default=os.path.join(BASE_DIR, 'db.sqlite3')), - 'USER': env('DATABASE_USER', default=None), - 'PASSWORD': env('DATABASE_PASSWORD', default=None), - 'HOST': env('DATABASE_HOST', default=None), - 'PORT': env('DATABASE_PORT', default=None), - } -} - -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', }, -] - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -STATIC_URL = '/static/' - -STATIC_ROOT = os.path.join(BASE_DIR, 'www', 'static') - -MEDIA_URL = '/media/' - -MEDIA_ROOT = os.path.join(BASE_DIR, 'www', 'media') - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - } - }, - 'root': { - 'handlers': ['console'], - 'level': env('DJANGO_LOG_LEVEL', default='DEBUG'), - } -} - -AUTH_USER_MODEL = 'api.LessPassUser' - -JWT_AUTH = { - 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), - 'JWT_ALLOW_REFRESH': True, -} - -REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ), - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - 'PAGE_SIZE': 1000, - 'DEFAULT_FILTER_BACKENDS': ( - 'rest_framework.filters.OrderingFilter', - 'rest_framework.filters.SearchFilter', - ), - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', - 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.SessionAuthentication', - ), - 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework.renderers.JSONRenderer', - 'rest_framework.renderers.BrowsableAPIRenderer', - ), - 'DEFAULT_PARSER_CLASSES': ( - 'rest_framework.parsers.JSONParser', - ), - 'TEST_REQUEST_DEFAULT_FORMAT': 'json' -} - - -class DisableMigrations(object): - def __contains__(self, item): - return True - - def __getitem__(self, item): - return None - - -TESTS_IN_PROGRESS = False -if 'test' in sys.argv[1:] or 'jenkins' in sys.argv[1:]: - logging.disable(logging.CRITICAL) - PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.MD5PasswordHasher', - ) - DEBUG = False - TEMPLATE_DEBUG = False - TESTS_IN_PROGRESS = True - MIGRATION_MODULES = DisableMigrations() - -DJOSER = { - 'PASSWORD_RESET_CONFIRM_URL': '#/password/reset/confirm/{uid}/{token}', - 'ACTIVATION_URL': '#/activate/{uid}/{token}' -} - -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True - -if DEBUG: - EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend') -else: - EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.smtp.EmailBackend') -DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'contact@lesspass.com') -EMAIL_HOST = os.getenv('EMAIL_HOST', 'localhost') -EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '') -EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '') -EMAIL_PORT = env.int('EMAIL_PORT', default=25) -EMAIL_SUBJECT_PREFIX = os.getenv('EMAIL_SUBJECT_PREFIX', '[LessPass] ') -EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=False) -EMAIL_USE_SSL = env.bool('EMAIL_USE_SSL', default=False) diff --git a/containers/database/lesspass/urls.py b/containers/database/lesspass/urls.py deleted file mode 100644 index 7ffb69f..0000000 --- a/containers/database/lesspass/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.conf.urls import include, url -from django.contrib import admin - -urlpatterns = [ - url(r'^api/', include('api.urls')), - url(r'^admin/', admin.site.urls), -] diff --git a/containers/database/lesspass/wsgi.py b/containers/database/lesspass/wsgi.py deleted file mode 100644 index bc4d7c4..0000000 --- a/containers/database/lesspass/wsgi.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lesspass.settings") - -application = get_wsgi_application() diff --git a/containers/database/manage.py b/containers/database/manage.py deleted file mode 100755 index 1571b63..0000000 --- a/containers/database/manage.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lesspass.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/containers/database/requirements.txt b/containers/database/requirements.txt deleted file mode 100644 index 51df6d5..0000000 --- a/containers/database/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -Django==1.11 -django-cors-middleware==1.3.1 -djangorestframework==3.6.2 -djangorestframework-jwt==1.10.0 -psycopg2==2.7.1 -gunicorn==19.7.1 -djoser==0.5.4 -envparse==0.2.0 -# tests -factory-boy==2.8.1 diff --git a/containers/database/supervisord.conf b/containers/database/supervisord.conf deleted file mode 100755 index a58d751..0000000 --- a/containers/database/supervisord.conf +++ /dev/null @@ -1,14 +0,0 @@ -[supervisord] -nodaemon=true -logfile=/dev/null -pidfile=/var/run/supervisord.pid - -[program:gunicorn] -directory=/backend -command=gunicorn lesspass.wsgi:application -w 2 -b :8000 -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 diff --git a/containers/docker-compose.yml b/containers/docker-compose.yml index bc1ed56..0e65cb9 100644 --- a/containers/docker-compose.yml +++ b/containers/docker-compose.yml @@ -1,11 +1,23 @@ version: "3" services: + db: + image: postgres:9.5 + volumes: + - postgresql:/var/lib/postgresql/data + backend: + build: ./backend + expose: + - '8000' + links: + - db + env_file: + - .env profiles: image: typesense/typesense:0.9.2 volumes: - profiles:/data - ports: - - 8108:8108 + expose: + - '8108' environment: API_KEY: dev-api-key SEARCH_ONLY_API_KEY: dev-search-only-api-key @@ -21,3 +33,7 @@ services: volumes: - ./webserver/ssl:/ssl - ./webserver:/webserver +volumes: + postgresql: + www: + profiles: \ No newline at end of file diff --git a/containers/webserver/.gitignore b/containers/webserver/.gitignore new file mode 100644 index 0000000..3758a7d --- /dev/null +++ b/containers/webserver/.gitignore @@ -0,0 +1 @@ +ssl \ No newline at end of file diff --git a/containers/webserver/generate_apache_conf.py b/containers/webserver/generate_apache_conf.py index 052c9a5..4ed6d09 100644 --- a/containers/webserver/generate_apache_conf.py +++ b/containers/webserver/generate_apache_conf.py @@ -11,7 +11,6 @@ if __name__ == "__main__": "SSL_CERTIFICATE_KEY_FILE": "/etc/httpd/ssl/private/%s.key" % fqdn, "DEBUG": os.environ.get("DEBUG", "0") == "1", } - print(context) jinja_template = Template(open("/webserver/lesspass.conf.j2").read()) with open("/etc/httpd/conf.d/lesspass.conf", "w") as f: diff --git a/containers/webserver/lesspass.conf.j2 b/containers/webserver/lesspass.conf.j2 index 2a5b48b..c7e609f 100644 --- a/containers/webserver/lesspass.conf.j2 +++ b/containers/webserver/lesspass.conf.j2 @@ -9,6 +9,15 @@ ServerName {{ FQDN }} ServerName www.{{ FQDN }} + ProxyPass /api/ http://backend:8000/api/ + ProxyPassReverse /api/ http://backend:8000/api/ + SSLEngine on + SSLCertificateFile {{ SSL_CERTIFICATE_FILE }} + SSLCertificateKeyFile {{ SSL_CERTIFICATE_KEY_FILE }} + + + + ServerName www.{{ FQDN }} ServerAlias {{ FQDN }} DocumentRoot /var/www/html