diff --git a/.env.sample b/.env.sample index 3ff8bb7..1daaccd 100644 --- a/.env.sample +++ b/.env.sample @@ -8,7 +8,19 @@ LD_HOST_DATA_DIR=./data # Must end with a slash `/` LD_CONTEXT_PATH= +# Username of the initial superuser to create, leave empty to not create one +LD_SUPERUSER_NAME= +# Password for the initial superuser, leave empty to disable credentials authentication and rely on proxy authentication instead +LD_SUPERUSER_PASSWORD= # Option to disable background tasks LD_DISABLE_BACKGROUND_TASKS=False # Option to disable URL validation for bookmarks completely LD_DISABLE_URL_VALIDATION=False +# Enables support for authentication proxies such as Authelia +LD_ENABLE_AUTH_PROXY=False +# Name of the request header that the auth proxy passes to the application to identify the user +# See docs/Options.md for more details +LD_AUTH_PROXY_USERNAME_HEADER= +# The URL that linkding should redirect to after a logout, when using an auth proxy +# See docs/Options.md for more details +LD_AUTH_PROXY_LOGOUT_URL= diff --git a/bookmarks/management/commands/create_initial_superuser.py b/bookmarks/management/commands/create_initial_superuser.py new file mode 100644 index 0000000..9f44a0f --- /dev/null +++ b/bookmarks/management/commands/create_initial_superuser.py @@ -0,0 +1,37 @@ +import os +import logging + +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Creates an initial superuser for a deployment using env variables" + + def handle(self, *args, **options): + User = get_user_model() + superuser_name = os.getenv('LD_SUPERUSER_NAME', None) + superuser_password = os.getenv('LD_SUPERUSER_PASSWORD', None) + + # Skip if option is undefined + if not superuser_name: + logger.info('Skip creating initial superuser, LD_SUPERUSER_NAME option is not defined') + return + + # Skip if user already exists + user_exists = User.objects.filter(username=superuser_name).exists() + if user_exists: + logger.info('Skip creating initial superuser, user already exists') + return + + user = User(username=superuser_name, is_superuser=True, is_staff=True) + + if superuser_password: + user.set_password(superuser_password) + else: + user.set_unusable_password() + + user.save() + logger.info('Created initial superuser') diff --git a/bookmarks/tests/test_create_initial_superuser_command.py b/bookmarks/tests/test_create_initial_superuser_command.py new file mode 100644 index 0000000..81caa3a --- /dev/null +++ b/bookmarks/tests/test_create_initial_superuser_command.py @@ -0,0 +1,45 @@ +import os +from unittest import mock + +from django.test import TestCase + +from bookmarks.models import User +from bookmarks.management.commands.create_initial_superuser import Command + + +class TestCreateInitialSuperuserCommand(TestCase): + + @mock.patch.dict(os.environ, {'LD_SUPERUSER_NAME': 'john', 'LD_SUPERUSER_PASSWORD': 'password123'}) + def test_create_with_password(self): + Command().handle() + + self.assertEqual(1, User.objects.count()) + + user = User.objects.first() + self.assertEqual('john', user.username) + self.assertTrue(user.has_usable_password()) + self.assertTrue(user.check_password('password123')) + + @mock.patch.dict(os.environ, {'LD_SUPERUSER_NAME': 'john'}) + def test_create_without_password(self): + Command().handle() + + self.assertEqual(1, User.objects.count()) + + user = User.objects.first() + self.assertEqual('john', user.username) + self.assertFalse(user.has_usable_password()) + + def test_create_without_options(self): + Command().handle() + + self.assertEqual(0, User.objects.count()) + + @mock.patch.dict(os.environ, {'LD_SUPERUSER_NAME': 'john', 'LD_SUPERUSER_PASSWORD': 'password123'}) + def test_create_multiple_times(self): + Command().handle() + Command().handle() + Command().handle() + + self.assertEqual(1, User.objects.count()) + diff --git a/bootstrap.sh b/bootstrap.sh index ef72f4c..fb184c0 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -10,6 +10,8 @@ mkdir -p data python manage.py migrate # Generate secret key file if it does not exist python manage.py generate_secret_key +# Create initial superuser if defined in options / environment variables +python manage.py create_initial_superuser # Ensure the DB folder is owned by the right user chown -R www-data: /etc/linkding/data diff --git a/docs/Options.md b/docs/Options.md index 75b89f8..705c00e 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -25,6 +25,22 @@ All options need to be defined as environment variables in the environment that ## List of options +### `LD_SUPERUSER_NAME` + +Values: `String` | Default = None + +When set, creates an initial superuser with the specified username when starting the container. +Does nothing if the user already exists. + +See [`LD_SUPERUSER_PASSWORD`](#ld_superuser_password) on how to configure the respective password. + +### `LD_SUPERUSER_PASSWORD` + +Values: `String` | Default = None + +The password for the initial superuser. +When left undefined, the superuser will be created without a usable password, which means the user can not authenticate using credentials / through the login form, and can only be authenticated using proxy authentication (see [`LD_ENABLE_AUTH_PROXY`](#ld_enable_auth_proxy)). + ### `LD_DISABLE_BACKGROUND_TASKS` Values: `True`, `False` | Default = `False`