Repository: paperless-ngx/paperless-ngx

Issue:
[BUG] Password reset after docker container restarted
*Copy from old repository*: https://github.com/jonaswinkler/paperless-ng/issues/1511

**Describe the bug**
I deployed Paperless-NG in TrueNAS via the TrueCharts integration. TrueCharts uses the official docker container and passes environment variables to configure the superuser.

I changed the admin password in the Django admin interface. However, after redeploying the application (for example due to an update) the password gets overridden by the initial password passed via environment variable.

**To Reproduce**
Steps to reproduce the behavior:
1. Deploy Paperless with credentials admin//secret
2. Open Paperless
3. Navigate to admin interface
4. Change password to "mysupersecretpassword"
5. Restart/update the docker container
6. Navigate to Paperless and try to login with admin/mysupersecretpassword
7. You can't login.

**Expected behavior**
The admin password should not be overridden by the initial password.

**Relevant information**
 - Version 
 - Installation method: **docker**
 - Any configuration changes you made in `docker-compose.yml`, `docker-compose.env` or `paperless.conf`. -

I think this is related to the admin user password reset when the docker container is started:
docker-entrypoint.sh calls docker-prepare.sh calls the manage_superuser mgmt command and there the password is updated:
https://github.com/jonaswinkler/paperless-ng/blob/master/src/documents/management/commands/manage_superuser.py#L29

Am I missing something?


Discussion:
@alexthamm this is by design. https://paperless-ngx.readthedocs.io/en/latest/configuration.html#configuration see the section of `PAPERLESS_ADMIN_USER` and `PAPERLESS_ADMIN_PASSWORD`. Otherwise, you are able to lock out the admin user. Maybe the only improvement here can be, to disable password changes for the admin user in UI and API.

The "Superuser" should only be used to configure paperless-ngx add (restricted) admins or employees users.
Yea, I think the point is you should set `PAPERLESS_ADMIN_PASSWORD` to 'mysupersecretpassword', problem solved, no?
Most of the compose services I'm running follow the pattern:

1. Check if admin account exists
2. IF not create it with the given password
3. If it's already existing, do nothing

I checked the django management command https://github.com/paperless-ngx/paperless-ngx/blob/main/src/documents/management/commands/manage_superuser.py

It would be a small change, to adopt it'S behaviour and I think from a security standpoint it's better.
The comments state that it has been the default behaviour until apparently someone changed the behaviour. And it has been changed without adopting the input checks, so I would call it broken.

So maybe we should cleanup.

I can create a PR that would change the django management command so it has following behaviour:

1. Define a New parameter: PAPERLESS_ADMIN_FORCE
2.  Check if super_user exists
3. Yes -> Only change password if PAPERLESS_ADMIN_FORCE
4. No -> Create Super User

Regards

Schnuffle
Hi all, thanks for your feedback!

However, I don't agree with the concept of setting the password somewhere readable (even iff the admin is supposed to have access to this information).
One of Django's best features is the user management. This includes storage of *securely hashed* passwords. Setting the password in the environment variable contradicts this idea.

@FrankStrieter 
> Otherwise, you are able to lock out the admin user. Maybe the only improvement here can be, to disable password changes for the admin user in UI and API.

Django has a management command and you can use the shell to override the admin/user password:
-> https://docs.djangoproject.com/en/3.2/topics/auth/default/#changing-passwords

IMO the management command should be documented and the password should never be touched by the environment/start scripts after the initial setup.

What do you think about this?
I happily aggree, The offer of a PAPERLESS_ADMIN_FORCE was to compromise. In the end it would be a shortcut to call the django manage.py inside the container.
I'm totally in favour of restricting dockers influence on the containers state purely to the setup.

The mentioned additional management command already exists and is used to create the super user. I linked to the code above.
So it's not a question of doing it, more about dropping it by using standard django manage commands or adopting it, so it has a sane behaviour
The advantage of having it, is that you have a precise place where you define the logic, in contrast to severall django commands.
The drawback, of course it's some lines of code.

My opinion:
Keep it with following logic:
If user exists -> do nothing
Else -> create super user


Regards

Schnuffle

So should I create a PR?

Regards

Schnuffle
Here's the new PR: #208 


The logic needed to become a bit more complex as the user "consumer" is created during migration 

The admin user with username "admin" and password from env variable PAPERLESS_ADMIN_PASSWORD is created when:

- There's no user or superuser called admin
- There's no superuser with other name
- PAPERLESS_ADMIN_PASSWORD is provided

When somebody reviews, please take a extra sharp look at the tests I added
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Please reopen. This is still an issue. If a user exists, this env var should do nothing.
Yep, the PR has to be rebased and reviewed still. 

Resolving patch:
diff --git a/src/documents/management/commands/manage_superuser.py b/src/documents/management/commands/manage_superuser.py
--- a/src/documents/management/commands/manage_superuser.py
+++ b/src/documents/management/commands/manage_superuser.py
@@ -11,7 +11,14 @@
 class Command(BaseCommand):
 
     help = """
-        Creates a Django superuser based on env variables.
+        Creates a Django superuser:
+        User named: admin
+        Email: root@localhost
+        with password based on env variable.
+        No superuser will be created, when:
+        - The username is taken already exists
+        - A superuser already exists
+        - PAPERLESS_ADMIN_PASSWORD is not set
     """.replace(
         "    ",
         "",
@@ -19,26 +26,41 @@ class Command(BaseCommand):
 
     def handle(self, *args, **options):
 
-        username = os.getenv("PAPERLESS_ADMIN_USER")
-        if not username:
-            return
-
+        username = os.getenv("PAPERLESS_ADMIN_USER", "admin")
         mail = os.getenv("PAPERLESS_ADMIN_MAIL", "root@localhost")
         password = os.getenv("PAPERLESS_ADMIN_PASSWORD")
 
-        # Check if user exists already, leave as is if it does
+        # Check if there's already a user called admin
         if User.objects.filter(username=username).exists():
-            user: User = User.objects.get_by_natural_key(username)
-            user.set_password(password)
-            user.save()
-            self.stdout.write(f"Changed password of user {username}.")
-        elif password:
-            # Create superuser based on env variables
-            User.objects.create_superuser(username, mail, password)
-            self.stdout.write(f'Created superuser "{username}" with provided password.')
+            self.stdout.write(
+                self.style.NOTICE(
+                    f"Did not create superuser, a user {username} already exists",
+                ),
+            )
+            return
+
+        # Check if any superuseruser
+        # exists already, leave as is if it does
+        if User.objects.filter(is_superuser=True).count() > 0:
+            self.stdout.write(
+                self.style.NOTICE(
+                    "Did not create superuser, the DB already contains superusers",
+                ),
+            )
+            return
+
+        if password is None:
+            self.stdout.write(
+                self.style.ERROR(
+                    "Please check if PAPERLESS_ADMIN_PASSWORD has been"
+                    " set in the environment",
+                ),
+            )
         else:
-            self.stdout.write(f'Did not create superuser "{username}".')
+            # Create superuser with password based on env variable
+            User.objects.create_superuser(username, mail, password)
             self.stdout.write(
-                'Make sure you specified "PAPERLESS_ADMIN_PASSWORD" in your '
-                '"docker-compose.env" file.',
+                self.style.SUCCESS(
+                    f'Created superuser "{username}" with provided password.',
+                ),
             )


Test patch:
diff --git a/src/documents/tests/test_management_superuser.py b/src/documents/tests/test_management_superuser.py
--- a/src/documents/tests/test_management_superuser.py
+++ b/src/documents/tests/test_management_superuser.py
@@ -1,67 +1,180 @@
 import os
-import shutil
+from io import StringIO
 from unittest import mock
 
 from django.contrib.auth.models import User
 from django.core.management import call_command
 from django.test import TestCase
-from documents.management.commands.document_thumbnails import _process_document
-from documents.models import Correspondent
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import Tag
 from documents.tests.utils import DirectoriesMixin
 
 
 class TestManageSuperUser(DirectoriesMixin, TestCase):
-    def reset_environment(self):
-        if "PAPERLESS_ADMIN_USER" in os.environ:
-            del os.environ["PAPERLESS_ADMIN_USER"]
-        if "PAPERLESS_ADMIN_PASSWORD" in os.environ:
-            del os.environ["PAPERLESS_ADMIN_PASSWORD"]
-
-    def setUp(self) -> None:
-        super().setUp()
-        self.reset_environment()
-
-    def tearDown(self) -> None:
-        super().tearDown()
-        self.reset_environment()
+    def call_command(self, environ):
+        out = StringIO()
+        with mock.patch.dict(os.environ, environ):
+            call_command(
+                "manage_superuser",
+                "--no-color",
+                stdout=out,
+                stderr=StringIO(),
+            )
+        return out.getvalue()
 
     def test_no_user(self):
-        call_command("manage_superuser")
+        """
+        GIVEN:
+            - Environment does not contain admin user info
+        THEN:
+            - No admin user is created
+        """
+
+        out = self.call_command(environ={})
 
-        # just the consumer user.
+        # just the consumer user which is created
+        # during migration
         self.assertEqual(User.objects.count(), 1)
         self.assertTrue(User.objects.filter(username="consumer").exists())
+        self.assertEqual(User.objects.filter(is_superuser=True).count(), 0)
+        self.assertEqual(
+            out,
+            "Please check if PAPERLESS_ADMIN_PASSWORD has been set in the environment\n",
+        )
 
     def test_create(self):
-        os.environ["PAPERLESS_ADMIN_USER"] = "new_user"
-        os.environ["PAPERLESS_ADMIN_PASSWORD"] = "123456"
-
-        call_command("manage_superuser")
-
-        user: User = User.objects.get_by_natural_key("new_user")
-        self.assertTrue(user.check_password("123456"))
-
-    def test_update(self):
-        os.environ["PAPERLESS_ADMIN_USER"] = "new_user"
-        os.environ["PAPERLESS_ADMIN_PASSWORD"] = "123456"
-
-        call_command("manage_superuser")
-
-        os.environ["PAPERLESS_ADMIN_USER"] = "new_user"
-        os.environ["PAPERLESS_ADMIN_PASSWORD"] = "more_secure_pwd_7645"
-
-        call_command("manage_superuser")
-
-        user: User = User.objects.get_by_natural_key("new_user")
-        self.assertTrue(user.check_password("more_secure_pwd_7645"))
+        """
+        GIVEN:
+            - Environment does contain admin user password
+        THEN:
+            - admin user is created
+        """
+
+        out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"})
+
+        # count is 2 as there's the consumer
+        # user already created during migration
+        user: User = User.objects.get_by_natural_key("admin")
+        self.assertEqual(User.objects.count(), 2)
+        self.assertTrue(user.is_superuser)
+        self.assertEqual(user.email, "root@localhost")
+        self.assertEqual(out, 'Created superuser "admin" with provided password.\n')
+
+    def test_some_superuser_exists(self):
+        """
+        GIVEN:
+            - A super user already exists
+            - Environment does contain admin user password
+        THEN:
+            - admin user is NOT created
+        """
+        User.objects.create_superuser("someuser", "root@localhost", "password")
+
+        out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"})
+
+        self.assertEqual(User.objects.count(), 2)
+        with self.assertRaises(User.DoesNotExist):
+            User.objects.get_by_natural_key("admin")
+        self.assertEqual(
+            out,
+            "Did not create superuser, the DB already contains superusers\n",
+        )
+
+    def test_admin_superuser_exists(self):
+        """
+        GIVEN:
+            - A super user already exists
+            - The existing superuser's username is admin
+            - Environment does contain admin user password
+        THEN:
+            - Password remains unchanged
+        """
+        User.objects.create_superuser("admin", "root@localhost", "password")
+
+        out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"})
+
+        self.assertEqual(User.objects.count(), 2)
+        user: User = User.objects.get_by_natural_key("admin")
+        self.assertTrue(user.check_password("password"))
+        self.assertEqual(out, "Did not create superuser, a user admin already exists\n")
+
+    def test_admin_user_exists(self):
+        """
+        GIVEN:
+            - A user already exists with the username admin
+            - Environment does contain admin user password
+        THEN:
+            - Password remains unchanged
+            - User is not upgraded to superuser
+        """
+
+        User.objects.create_user("admin", "root@localhost", "password")
+
+        out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"})
+
+        self.assertEqual(User.objects.count(), 2)
+        user: User = User.objects.get_by_natural_key("admin")
+        self.assertTrue(user.check_password("password"))
+        self.assertFalse(user.is_superuser)
+        self.assertEqual(out, "Did not create superuser, a user admin already exists\n")
 
     def test_no_password(self):
-        os.environ["PAPERLESS_ADMIN_USER"] = "new_user"
-
-        call_command("manage_superuser")
+        """
+        GIVEN:
+            - No environment data is set
+        THEN:
+            - No user is created
+        """
+        out = self.call_command(environ={})
 
         with self.assertRaises(User.DoesNotExist):
-            User.objects.get_by_natural_key("new_user")
+            User.objects.get_by_natural_key("admin")
+        self.assertEqual(
+            out,
+            "Please check if PAPERLESS_ADMIN_PASSWORD has been set in the environment\n",
+        )
+
+    def test_user_email(self):
+        """
+        GIVEN:
+            - Environment does contain admin user password
+            - Environment contains user email
+        THEN:
+            - admin user is created
+        """
+
+        out = self.call_command(
+            environ={
+                "PAPERLESS_ADMIN_PASSWORD": "123456",
+                "PAPERLESS_ADMIN_MAIL": "hello@world.com",
+            },
+        )
+
+        user: User = User.objects.get_by_natural_key("admin")
+        self.assertEqual(User.objects.count(), 2)
+        self.assertTrue(user.is_superuser)
+        self.assertEqual(user.email, "hello@world.com")
+        self.assertEqual(user.username, "admin")
+        self.assertEqual(out, 'Created superuser "admin" with provided password.\n')
+
+    def test_user_username(self):
+        """
+        GIVEN:
+            - Environment does contain admin user password
+            - Environment contains user username
+        THEN:
+            - admin user is created
+        """
+
+        out = self.call_command(
+            environ={
+                "PAPERLESS_ADMIN_PASSWORD": "123456",
+                "PAPERLESS_ADMIN_MAIL": "hello@world.com",
+                "PAPERLESS_ADMIN_USER": "super",
+            },
+        )
+
+        user: User = User.objects.get_by_natural_key("super")
+        self.assertEqual(User.objects.count(), 2)
+        self.assertTrue(user.is_superuser)
+        self.assertEqual(user.email, "hello@world.com")
+        self.assertEqual(user.username, "super")
+        self.assertEqual(out, 'Created superuser "super" with provided password.\n')