flat icon indicating copy to clipboard operation
flat copied to clipboard

Dubious ownership of annotations

Open savary opened this issue 1 year ago • 13 comments

Our FLAT server goes down from time to time and we are trying to establish the reasons for that. One such case happened yesterday evening. I looked into the logs of the docker (sudo docker logs -n 1000 flat). An annotator was working on FLAT, then she seems to have logged out and logged back in. Then this error occurred:

fatal: detected dubious ownership in repository at '/data/annotations' To add an exception for this directory, call: git config --global --add safe.directory /data/annotations

This is likely due to our versioning of the annotations via an external git repository. It seems that the error is not that problematic as discussed here.

But I checked the access permissions in our annotation directory and they do not look very homogeneous:

parseme@parseme:~/annotations$ cd .. parseme@parseme:~$ cd annotations/ parseme@parseme:~/annotations$ ls -lia total 1752 5505187 drwxrwsr-x 351 parseme parseme 20480 nov. 13 18:49 . 5505059 drwxr-x--- 9 parseme parseme 4096 août 7 11:43 .. 5637212 drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 Abbas 5636773 drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 abdelati.hawwari 5637259 drwxr-xr-x 3 parseme parseme 4096 janv. 10 2024 abigail.walsh 5637425 drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 adela.tocaru 5767548 drwxr-sr-x 2 root parseme 4096 juil. 10 2024 adelina.cerpja 5767563 drwxr-sr-x 2 root parseme 4096 juil. 10 2024 adina.duca 5505488 drwxr-xr-x 4 parseme parseme 4096 janv. 10 2024 agata.savary 5637082 drwxr-xr-x 16 parseme parseme 4096 oct. 8 10:26 agata.savary.annotator 5506634 drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 agata.savary.test 5767594 drwxr-sr-x 2 root parseme 4096 juil. 5 2024 agata.savary.unidive 5636803 drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 aggelfoto123 5767576 drwxr-sr-x 2 root parseme 4096 juil. 17 10:38 agute.klints 5636807 drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 ainara.estarrona 5637002 drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 aixiu.an.zh ... parseme@parseme:~/annotations$ ls -l ./yalda.yarandi/final total 22000 -rw-r--r-- 1 root root 2373753 août 12 16:01 dev.udpipe-2.10-xpos-to-deprel.folia.xml -rw-r--r-- 1 root root 2001046 août 12 16:01 test.udpipe-2.10-xpos-to-deprel.folia.xml -rw-r--r-- 1 root root 12274118 août 12 16:01 train.udpipe-2.10-xpos-to-deprel.folia.xml -rw-r--r-- 1 root root 1073626 août 12 16:01 tree_bank_without_VMWE.folia.xml -rw-r--r-- 1 root root 4792400 août 12 16:01 tree_bank_with_VMWE.folia.xml parseme@parseme:~/annotations$ ls -l ./jaka.cibej/ total 328 -rw-r--r-- 1 parseme parseme 333884 janv. 10 2024 parseme_sl_ssj500k_13412_13511_noIDs.parsemetsv.folia.xml

Some directories have root as owner, another have parseme. In the former case the permissions are drwxr-sr-x, in the latter they are drwxr-xr-x. Similarly, some files have root as owner, some others have parseme.

Given that users are always added via DJANGO interface, what causes the difference? And are the correct owners and permissions? Could this be a reason for the unstability of the server?

savary avatar Jan 14 '25 14:01 savary

Such inconsistent ownership occurs when the git repository is shared amongst multiple (unix) users (in this case root and parseme). Note that this does not refer to FLAT users but unix users.

Inside the FLAT container foliadocserve runs under UID/GID 100. How this maps on the actual host depends a bit on your configuration. You can check this on the host with ps aux | grep foliadocserve.

Ownership can get messed up if the FLAT container wasn't always consistently started in the same way but as different users, or if the git repository at the document root is pulled by some external process rather than foliadocserve and that runs under another user. If you do a git pull, take care to use the same user foliadocserve runs under.

You'll want to reset the ownership to make it consistent again using chown -R on the document root, setting it to the UID/GID foliadocserve runs under (which I guess is parseme in your case, but make sure to check). Running as root, even within the container, is not recommended.

This definitely can be a cause of errors, but that would be permission denied errors where foliadocserve can't write a file to disk. Actually I'm thinking back and it's quite likely this is the cause of https://github.com/proycon/flat/issues/191 .

Could this be a reason for the unstability of the server?

If you say the FLAT server goes down from time to time do you really mean the actual physical server goes down?? Or just FLAT/foliadocserve process. In any case, none of those should really occur because of this.

proycon avatar Jan 15 '25 21:01 proycon

Such inconsistent ownership occurs when the git repository is shared amongst multiple (unix) users (in this case root and parseme). Note that this does not refer to FLAT users but unix users.

Right.

Inside the FLAT container foliadocserve runs under UID/GID 100. How this maps on the actual host depends a bit on your configuration. You can check this on the host with ps aux | grep foliadocserve.

I'm not sure how to find it from this command? parseme@parseme:~$ ps aux | grep foliadocserve root 3849 0.0 0.0 820 0 ? Ss janv.14 0:00 runsv foliadocserve root 4119 0.0 0.0 2224 516 ? S janv.14 0:01 tee /data/BKP/2025-01-14_13:19:47/foliadocserve.stdout root 4120 0.0 0.0 2224 544 ? S janv.14 0:00 tee /data/BKP/2025-01-14_13:19:47/foliadocserve.stderr root 4121 1.3 0.8 178152 134876 ? Sl janv.14 141:18 /usr/bin/python3 /usr/bin/foliadocserve -d /data/annotations --log /data/BKP/2025-01-14_13:19:47/foliadocserve.log -p 8080 --git parseme 4144594 0.0 0.0 6612 2424 pts/0 S+ 13:57 0:00 grep --color=auto foliadocserve

Note that the parseme user has an UID which does not appear above: parseme@parseme:~$ id -u parseme 1004

Ownership can get messed up if the FLAT container wasn't always consistently started in the same way but as different users, or if the git repository at the document root is pulled by some external process rather than foliadocserve and that runs under another user. If you do a git pull, take care to use the same user foliadocserve runs under.

Sorry, it is not quite clear to me. We indeed use git to version the annotations folder:

parseme@parseme:~$ cd annotations/ parseme@parseme:~/annotations$ git config --get remote.origin.url [email protected]:parseme/annotations.git

I think it is launched by cron, since in the logs I see:

sudo grep CRON /var/log/syslog ... Jan 23 11:00:01 parseme CRON[3871233]: (parseme) CMD (/home/parseme/annotations/countMWEs.py \ >/home/parseme/annotations/.mwe-count.json) Jan 23 11:00:01 parseme CRON[3871234]: (tuanbui) CMD (/home/parseme/parseme/updateGitlabWithAnnotations.sh) Jan 23 11:00:01 parseme CRON[3871235]: (parseme) CMD \ (/home/parseme/annotations/.settingsCommitHack/updateGitlabWithSettings.sh) Jan 23 11:00:01 parseme CRON[3871226]: (CRON) info (No MTA installed, discarding output) Jan 23 11:00:01 parseme CRON[3871238]: (parseme) CMD (/home/parseme/annotations/updateGitlabWithAnnotations.sh)

And here is what the script contains:

parseme@parseme:~/annotations$ cat updateGitlabWithAnnotations.sh #! /bin/sh export LANGUAGE=C export LANG=C export LC_ALL=C HERE="$(cd "$(dirname "$0")" && pwd)" DATE="$(date --rfc-3339=s)"

set -o nounset # Using "$UNDEF" var raises error set -o errexit # Exit on error, do not continue quietly

cd "$HERE" exec 1>.git-commit-log.stdout exec 2>.git-commit-log.stderr

echo "$DATE" >&2 git pull modified_userdirs="$(git status | grep modified: | awk '{print $2}' | grep '/' | sed 's@/.*@@g' | sort -u | awk 'BEGIN{ORS=" "} 1' | sed 's@ *$@@g')" git add * # commit all files in $HERE that do not start with "." git commit -am "Auto-commit: $DATE" -m "Modified: [$modified_userdirs]" || true git push

How do I know out of this which user launches git pull and git push?

In the logs it looks like it is tuanbui, right (line 3871234 above)? Which is probably not what is expected. But even so, the folders and files in annotations\ belong either to parseme or to root, not to tuanbui.

You'll want to reset the ownership to make it consistent again using chown -R on the document root, setting it to the UID/GID foliadocserve runs under (which I guess is parseme in your case, but make sure to check). Running as root, even within the container, is not recommended.

I understand, but again I'm not quite sure. The docker image was set up by a colleague who moved somewhere else. He left a good documentation though and there I see that to restart the server I have to run: sudo docker ps Does it mean that the annotation directories and files will have root as owner (I don't think so).

This definitely can be a cause of errors, but that would be permission denied errors where foliadocserve can't write a file to disk. Actually I'm thinking back and it's quite likely this is the cause of #191

OK, so I have now given ownership to parseme to all the folders and files in annotations? parseme@parseme:~/annotations$ sudo chown -R parseme:parseme * ... parseme@parseme:~/annotations$ ll | head -10 total 1776 drwxrwsr-x 357 parseme parseme 20480 janv. 14 22:32 ./ drwxr-x--- 9 parseme parseme 4096 janv. 14 15:49 ../ drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 Abbas/ drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 abdelati.hawwari/ drwxr-xr-x 3 parseme parseme 4096 janv. 10 2024 abigail.walsh/ drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 adela.tocaru/ drwxr-sr-x 2 parseme parseme 4096 juil. 10 2024 adelina.cerpja/ drwxr-sr-x 2 parseme parseme 4096 juil. 10 2024 adina.duca/ drwxr-xr-x 4 parseme parseme 4096 janv. 10 2024 agata.savary/`

And what would be the right permissions? Note that currently we still have drwxr-xr-x and drwxr-sr-x in folders. And for files we have: parseme@parseme:~/annotations$ ll */* | cut -d' ' -f1 | grep -E '^\-' | sort -u -rw------- -rw-r--r-- -rw-rw-r-- What does FLAT require?

Could this be a reason for the unstability of the server?

If you say the FLAT server goes down from time to time do you really mean the actual physical server goes down?? Or just FLAT/foliadocserve process. In any case, none of those should really occur because of this.

I'm actually not quite sure. Maybe we also have issues with the physical server from time to time. I will go on following this issue.

Thank you very much, this is enlightening.

savary avatar Jan 23 '25 10:01 savary

I'm not sure how to find it from this command?

@.***:~$ ps aux | grep foliadocserve root 3849 0.0 0.0 820 0 ? Ss janv.14 0:00 runsv foliadocserve root 4119 0.0 0.0 2224 516 ? S janv.14 0:01 tee /data/BKP/2025-01-14_13:19:47/foliadocserve.stdout root 4120 0.0 0.0 2224 544 ? S janv.14 0:00 tee /data/BKP/2025-01-14_13:19:47/foliadocserve.stderr root 4121 1.3 0.8 178152 134876 ? Sl janv.14 141:18 /usr/bin/python3 /usr/bin/foliadocserve -d /data/annotations --log /data/BKP/2025-01-14_13:19:47/foliadocserve.log -p 8080 --git parseme 4144594 0.0 0.0 6612 2424 pts/0 S+ 13:57 0:00 grep --color=auto foliadocserve`

The last one above the grep is the one I meant. It shows foliadocserve running as root, which is definitely not recommended.

Ownership can get messed up if the FLAT container wasn't always consistently started in the same way but as different users, or if the git repository at the document root is pulled by some external process rather than foliadocserve and that runs under another user. If you do a git pull, take care to use the same user foliadocserve runs under.

Sorry, it is not quite clear to me. We indeed use git to version the annotations folder:

@.:~$ cd annotations/ ***@***.***:~/annotations$ git config --get remote.origin.url @.:parseme/annotations.git`

I think it is launched by cron, since in the logs I see:

sudo grep CRON /var/log/syslog ... Jan 23 11:00:01 parseme CRON[3871233]: (parseme) CMD (/home/parseme/annotations/countMWEs.py \ >/home/parseme/annotations/.mwe-count.json) Jan 23 11:00:01 parseme CRON[3871234]: (tuanbui) CMD (/home/parseme/parseme/updateGitlabWithAnnotations.sh) Jan 23 11:00:01 parseme CRON[3871235]: (parseme) CMD \ (/home/parseme/annotations/.settingsCommitHack/updateGitlabWithSettings.sh) Jan 23 11:00:01 parseme CRON[3871226]: (CRON) info (No MTA installed, discarding output) Jan 23 11:00:01 parseme CRON[3871238]: (parseme) CMD (/home/parseme/annotations/updateGitlabWithAnnotations.sh)

Yes, indeed

And here is what the script contains:

@.***:~/annotations$ cat updateGitlabWithAnnotations.sh #! /bin/sh export LANGUAGE=C export LANG=C export LC_ALL=C HERE="$(cd "$(dirname "$0")" && pwd)" DATE="$(date --rfc-3339=s)"`

set -o nounset # Using "$UNDEF" var raises error set -o errexit # Exit on error, do not continue quietly

cd "$HERE" exec 1>.git-commit-log.stdout exec 2>.git-commit-log.stderr

echo "$DATE" >&2 git pull modified_userdirs="$(git status | grep modified: | awk '{print $2}' | grep '/' | sed 's@/.*@@g' | sort -u | awk 'BEGIN{ORS=" "} 1' | sed 's@ *$@@g')" git add * # commit all files in $HERE that do not start with "." git commit -am "Auto-commit: $DATE" -m "Modified: [$modified_userdirs]" || true git push

How do I know out of this which user launches git pull and git push?

Your cron log showed that, there were two, one run by parseme and one tuanbui.

In the logs it looks like it is tuanbui, right (line 3871234 above)? Which is probably not what is expected.

Yes, the one in /home/parseme/annotations is run by 'parseme', which looks good. tuanbui runs on a different subdirectory.

But even so, the folders and files in annotations\ belong either to parseme or to root, not to tuanbui.

Yes, the ones by root are caused by foliadocserve.

You'll want to reset the ownership to make it consistent again using chown -R on the document root, setting it to the UID/GID foliadocserve runs under (which I guess is parseme in your case, but make sure to check). Running as root, even within the container, is not recommended.

I understand, but again I'm not quite sure. The docker image was set up by a colleague who moved somewhere else. He left a good documentation though and there I see that to restart the server I have to run: sudo docker ps Does it mean that the annotation directories and files will have root as owner (I don't think so).

docker ps will only show the running containers, not restart any, but that suggests to me that the container is likely also started via sudo docker run and runs as root.

I'm not entirely sure why foliadocservice is not running as UID 100 but as 0 (root). But this is likely a problem in the docker setup on your server. One if the most cumbersome things of docker management is mapping UIDs between host and containers and running rootless containers. You can find the relevant documentation here:

  • https://docs.docker.com/engine/security/userns-remap/
  • https://docs.docker.com/engine/security/rootless/

The most secure way is to setup the parseme user to run docker containers without requiring root privileges. The UID inside the container can be controlled by the UWSGI_UID environment variable you pass into the container (I just committed one extra fix for that and republished the docker image). The UID on the host should map to a UID in the container (a so-called subuid). If you have a system administrator around with docker experience, he/she might be able to help navigate this maze.

OK, so I have now given ownership to parseme to all the folders and files in annotations? ***@***.***:~/annotations$ sudo chown -R parseme:parseme * ... @.***:~/annotations$ ll | head -10 total 1776 drwxrwsr-x 357 parseme parseme 20480 janv. 14 22:32 ./ drwxr-x--- 9 parseme parseme 4096 janv. 14 15:49 ../ drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 Abbas/ drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 abdelati.hawwari/ drwxr-xr-x 3 parseme parseme 4096 janv. 10 2024 abigail.walsh/ drwxr-xr-x 2 parseme parseme 4096 janv. 10 2024 adela.tocaru/ drwxr-sr-x 2 parseme parseme 4096 juil. 10 2024 adelina.cerpja/ drwxr-sr-x 2 parseme parseme 4096 juil. 10 2024 adina.duca/ drwxr-xr-x 4 parseme parseme 4096 janv. 10 2024 agata.savary/`

And what would be the right permissions? Note that currently we still have drwxr-xr-x and drwxr-sr-x in folders. And for files we have: @.***:~/annotations$ ll / | cut -d' ' -f1 | grep -E '^-' | sort -u -rw------- -rw-r--r-- -rw-rw-r--` What does FLAT require?

Things should be readable and writable for the user foliadocserve runs under (which is now root, so that works with the current permissions, but is a bad idea).

proycon avatar Jan 24 '25 15:01 proycon

Thanks for all the details. I'm talking to my IT staff about it.

savary avatar Feb 07 '25 12:02 savary

Hi @proycon Martin. It took us some time but we did delve into the two links you sent above. Obada (@obadas) from my IT team tried to install a test FLAT server in rootless mode, following your link. The installations went well but he didn’t succeed in launching the server. The error was 500 and the logs show below that the connection with the database fails. When the rootless mode is disabled, all seems to work fine.:

Traceback (most recent call last): File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 289, in ensure_connection self.connect() File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner return func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 270, in connect self.connection = self.get_new_connection(conn_params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner return func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/backends/mysql/base.py", line 247, in get_new_connection connection = Database.connect(**conn_params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/MySQLdb/__init__.py", line 121, in Connect return Connection(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/MySQLdb/connections.py", line 200, in __init__ super().__init__(*args, **kwargs2) MySQLdb.OperationalError: (2002, "Can't connect to local server through socket '/run/mysqld/mysqld.sock' (111)")

The above exception was the direct cause of the following exception:

Traceback (most recent call last): File "/usr/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner response = get_response(request) ^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/auth/decorators.py", line 22, in _wrapper_view if test_func(request.user): ^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/auth/decorators.py", line 51, in <lambda> lambda u: u.is_authenticated, ^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/utils/functional.py", line 266, in inner self._setup() File "/usr/lib/python3.12/site-packages/django/utils/functional.py", line 419, in _setup self._wrapped = self._setupfunc() ^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/auth/middleware.py", line 25, in <lambda> request.user = SimpleLazyObject(lambda: get_user(request)) ^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/auth/middleware.py", line 11, in get_user request._cached_user = auth.get_user(request) ^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/auth/__init__.py", line 191, in get_user user_id = _get_user_session_key(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/auth/__init__.py", line 60, in _get_user_session_key return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY]) ~~~~~~~~~~~~~~~^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/sessions/backends/base.py", line 53, in __getitem__ return self._session[key] ^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/sessions/backends/base.py", line 192, in _get_session self._session_cache = self.load() ^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/sessions/backends/db.py", line 42, in load s = self._get_session_from_db() ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/contrib/sessions/backends/db.py", line 32, in _get_session_from_db return self.model.objects.get( ^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/models/manager.py", line 87, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/models/query.py", line 633, in get num = len(clone) ^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/models/query.py", line 380, in __len__ self._fetch_all() File "/usr/lib/python3.12/site-packages/django/db/models/query.py", line 1881, in _fetch_all self._result_cache = list(self._iterable_class(self)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/models/query.py", line 91, in __iter__ results = compiler.execute_sql( ^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1560, in execute_sql cursor = self.connection.cursor() ^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner return func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 330, in cursor return self._cursor() ^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 306, in _cursor self.ensure_connection() File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner return func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 288, in ensure_connection with self.wrap_database_errors: ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/utils.py", line 91, in __exit__ raise dj_exc_value.with_traceback(traceback) from exc_value File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 289, in ensure_connection self.connect() File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner return func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 270, in connect self.connection = self.get_new_connection(conn_params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner return func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/django/db/backends/mysql/base.py", line 247, in get_new_connection connection = Database.connect(**conn_params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/MySQLdb/__init__.py", line 121, in Connect return Connection(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/site-packages/MySQLdb/connections.py", line 200, in __init__ super().__init__(*args, **kwargs2) django.db.utils.OperationalError: (2002, "Can't connect to local server through socket '/run/mysqld/mysqld.sock' (111)")

savary avatar Feb 28 '25 09:02 savary

As to usermap, @ObadaS followed your second link to launch the server as parseme user. Here, we face the problem of unauthorised access to files in the /home/parseme/BKP folder. Here are out logs:

Starting foliadocserve... mkdir: cannot create directory ‘/data’: Permission denied chown: cannot access '/data/annotations': Permission denied chmod: cannot access '/data/annotations': Permission denied rm: cannot remove '/data/BKP/latest': Permission denied mkdir: cannot create directory ‘/data/BKP/2025-02-26_10:27:42’: Permission denied ./run: cd: line 24: can't cd to /data/BKP/2025-02-26_10:27:42: Permission denied coreutils: failed to create symbolic link '../latest': File exists tee: '/data/BKP/2025-02-26_10:27:42/foliadocserve.stdout': Permission denied tee: '/data/BKP/2025-02-26_10:27:42/foliadocserve.stderr': Permission denied Traceback (most recent call last): File "/usr/bin/foliadocserve", line 8, in <module> sys.exit(main()) ^^^^^^ File "/usr/lib/python3.12/site-packages/foliadocserve/foliadocserve.py", line 1033, in main logfile = open(args.logfile,'a',encoding='utf-8') ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PermissionError: [Errno 13] Permission denied: '/data/BKP/2025-02-26_10:27:42/foliadocserve.log'

savary avatar Feb 28 '25 09:02 savary

In each case we used the latest version of the docker image.

Any further hints would be most welcome. Many thanks!

savary avatar Feb 28 '25 09:02 savary

On Fri Feb 28, 2025 at 10:02 AM CET, Agata Savary wrote:

savary left a comment (proycon/flat#193)

Hi @proycon Martin. It took us some time but we did delve into the two links you sent above. Obada @.***) from my IT team tried to install a test FLAT server in rootless mode, following your link. The installations went well but he didn’t succeed in launching the server. The error was 500 and the logs show below that the connection with the database fails. When the rootless mode is disabled, all seems to work fine.:

django.db.utils.OperationalError: (2002, "Can't connect to local server through socket '/run/mysqld/mysqld.sock' (111)")

What did you set the environment variables FLAT_DATABASE, FLAT_DATABASE_ENGINE and FLAT_DATABASE_HOST to? The logs say it's trying to access a local database via a socket, so that means the db is local to the container, but there is no MySQL in the FLAT container. Are you sure the configuration is the same between the rootless and root versions you've been trying? Make sure your MySQl/Mariadb server is accessible via TCP from the container.

As to usermap, @ObadaS followed your second link to launch the server as parseme user. Here, we face the problem of unauthorised access to files in the /home/parseme/BKP folder. Here are out logs:

Starting foliadocserve... mkdir: cannot create directory ‘/data’: Permission denied chown: cannot access '/data/annotations': Permission denied chmod: cannot access '/data/annotations': Permission denied rm: cannot remove '/data/BKP/latest': Permission denied mkdir: cannot create directory ‘/data/BKP/2025-02-26_10:27:42’: Permission denied ./run: cd: line 24: can't cd to /data/BKP/2025-02-26_10:27:42: Permission denied coreutils: failed to create symbolic link '../latest': File exists tee: '/data/BKP/2025-02-26_10:27:42/foliadocserve.stdout': Permission denied tee: '/data/BKP/2025-02-26_10:27:42/foliadocserve.stderr': Permission denied Traceback (most recent call last): File "/usr/bin/foliadocserve", line 8, in <module> sys.exit(main()) ^^^^^^ File "/usr/lib/python3.12/site-packages/foliadocserve/foliadocserve.py", line 1033, in main logfile = open(args.logfile,'a',encoding='utf-8') ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PermissionError: [Errno 13] Permission denied: '/data/BKP/2025-02-26_10:27:42/foliadocserve.log'

That indicates that the mapping wasn't correct. I can't see from the log what UIDs/GIDs are used on both ends. What you can do is, when the container is running, try to see what UID the foliadocserve process is using. If you chown the files to that UID they should be readable/writable from the container.

I hope this helps a bit, UID mappings can indeed be complicated.

proycon avatar Mar 03 '25 11:03 proycon

Hello,

What did you set the environment variables FLAT_DATABASE, FLAT_DATABASE_ENGINE and FLAT_DATABASE_HOST to? The logs say it's trying to access a local database via a socket, so that means the db is local to the container, but there is no MySQL in the FLAT container. Are you sure the configuration is the same between the rootless and root versions you've been trying? Make sure your MySQl/Mariadb server is accessible via TCP from the container.

For the tests I made, I copied all the files from the production server so that I can have the closest possible configuration. Since it works on the production server, the TCP connection should also work correctly, especially since it works fine when I disable rootless Docker.

That indicates that the mapping wasn't correct. I can't see from the log what UIDs/GIDs are used on both ends. What you can do is, when the container is running, try to see what UID the foliadocserve process is using. If you chown the files to that UID they should be readable/writable from the container.

I hope this helps a bit, UID mappings can indeed be complicated.

I used the guide to remap docker to be used by parseme, but foliadocserve is still being run by root. From what I understand from the previous comments, foliadocserve should be launched by uwsgi instead of root ? I am not sure why it's not working as intended on our end.

ObadaS avatar Mar 03 '25 13:03 ObadaS

On Mon Mar 3, 2025 at 2:35 PM CET, Obada Haddad-Soussac wrote:

For the tests I made, I copied all the files from the production server so that I can have the closest possible configuration.

Yes, that's a good approach indeed.

Since it works on the production server, the TCP connection should also work correctly, especially since it works fine when I disable rootless Docker.

It's a bit hard for me to judge your configuration from here. It's indeed strange that the database connection is a problem when switching between rootless and non-rootless.

That indicates that the mapping wasn't correct. I can't see from the log what UIDs/GIDs are used on both ends. What you can do is, when the container is running, try to see what UID the foliadocserve process is using. If you chown the files to that UID they should be readable/writable from the container.

I hope this helps a bit, UID mappings can indeed be complicated.

I used the guide to remap docker to be used by parseme, but foliadocserve is still being run by root. From what I understand from the previous comments, foliadocserve should be launched by uwsgi instead of root ? I am not sure why it's not working as intended on our end.

The foliadocserve python process should run as a a non-root user yes (both from the host perspective as from the container perspective). If it runs as root then something is wrong with the rootless container setup/user map. Foliadocserve is not launched by uwsgi technically, FLAT is, but both run under the same UID though (configurable via environment variable UWSGI_UID).

Here's an example of how the process list can look from the host's perspective, but note that I always use podman instead of docker (podman was designed a bit better with rootless containers in mind, and is mostly docker compatibly):

$ ps aux | grep flat
proycon   328300  0.0  0.0 2672192 44220 pts/4   Sl+  17:38   0:00 /usr/bin/podman run -t -i -p 8080:80 --env FLAT_DEBUG=1 proycon/flat:dev
proycon   328317  0.0  0.1 3328124 69400 pts/4   Sl+  17:38   0:00 /usr/bin/podman run -t -i -p 8080:80 --env FLAT_DEBUG=1 proycon/flat:dev
proycon   328389  0.0  0.0   1776  1160 ?        S    17:38   0:00 sudo -u nginx foliadocserve -d /data/flat.docroot --log /dev/stdout --git --gitshare false --expirationtime 120 -p 8080
100099    328390  0.2  0.0  76332 37728 ?        Sl   17:38   0:01 /usr/bin/python3 /usr/bin/foliadocserve -d /data/flat.docroot --log /dev/stdout --git --gitshare false --expirationtime 120 -p 8080
100099    328451  0.0  0.0  51328 40392 ?        S    17:38   0:00 uwsgi --plugin python3 --uid 100 --gid 100 --master --socket 127.0.0.1:8888 --wsgi-file /etc/flat.wsgi --processes 2 --single-interpreter --threads 2 --manage-script-name
100099    328458  0.0  0.0  51484 33772 ?        Sl   17:38   0:00 uwsgi --plugin python3 --uid 100 --gid 100 --master --socket 127.0.0.1:8888 --wsgi-file /etc/flat.wsgi --processes 2 --single-interpreter --threads 2 --manage-script-name
100099    328460  0.0  0.0  51484 33772 ?        Sl   17:38   0:00 uwsgi --plugin python3 --uid 100 --gid 100 --master --socket 127.0.0.1:8888 --wsgi-file /etc/flat.wsgi --processes 2 --single-interpreter --threads 2 --manage-script-name

In /etc/subuid I have:

proycon:100000:65536

UID 100 in the container then maps to 100000 + 100 - 1 on the host.

proycon avatar Mar 03 '25 16:03 proycon

I have some updates on this issue. I'm using the docker usermap service. I managed to make foliadocserve run using the user that I want (I changed /etc/subuid so that Root in the container is mapped to the parseme account on the host). Which means that foliadocserve now runs as root in the container which is fine for the host too.

However, when I try to launch the server, it doesn't successfully get past these steps :

Creating FLAT superuser and (re)setting password...
Updating existing password for superuser.
Starting FLAT via uwsgi...
*** Starting uWSGI 2.0.27 (64bit) on [Tue Mar 25 09:19:14 2025] ***
compiled with version: 14.2.0 on 22 October 2024 13:39:35
os: Linux-5.10.0-20-cloud-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13)
nodename: a1dd36fec89e
machine: x86_64
clock source: unix
pcre jit disabled
detected number of CPU cores: 6
current working directory: /etc/service/uwsgi
detected binary path: /usr/sbin/uwsgi
group 0 not found.
Applying migrations...
System check identified some issues:

WARNINGS:
metadata.MetadataIndex: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
	HINT: Configure the DEFAULT_AUTO_FIELD setting or the AppConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
users.ReadPermissions: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
	HINT: Configure the DEFAULT_AUTO_FIELD setting or the AppConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
users.WritePermissions: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
	HINT: Configure the DEFAULT_AUTO_FIELD setting or the AppConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
Operations to perform:
  Synchronize unmigrated apps: editor, messages, metadata, staticfiles, structureeditor, viewer
  Apply all migrations: admin, auth, contenttypes, sessions, sites, users
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
Running migrations:
  No migrations to apply.

It keeps repeating this over and over again. I tried adding --env FLAT_DEBUG=1 but it changed nothing in the logs.

I'm not sure if this is a permission problem again or if it's something else entirely.

ObadaS avatar Mar 25 '25 10:03 ObadaS

On Tue Mar 25, 2025 at 11:14 AM CET, Obada Haddad-Soussac wrote:

I have some updates on this issue. I'm using the docker usermap service. I managed to make foliadocserve run using the user that I want (I changed /etc/subuid so that Root in the container is mapped to the parseme account on the host). Which means that foliadocserve now runs as root in the container which is fine for the host too.

Great progress, that sounds good.

However, when I try to launch the server, it doesn't successfully get past these steps :

Creating FLAT superuser and (re)setting password...
Updating existing password for superuser.
Starting FLAT via uwsgi...
*** Starting uWSGI 2.0.27 (64bit) on [Tue Mar 25 09:19:14 2025] ***
compiled with version: 14.2.0 on 22 October 2024 13:39:35
os: Linux-5.10.0-20-cloud-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13)
nodename: a1dd36fec89e
machine: x86_64
clock source: unix
pcre jit disabled
detected number of CPU cores: 6
current working directory: /etc/service/uwsgi
detected binary path: /usr/sbin/uwsgi
group 0 not found.
Applying migrations...
System check identified some issues:

WARNINGS:
metadata.MetadataIndex: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
	HINT: Configure the DEFAULT_AUTO_FIELD setting or the AppConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
users.ReadPermissions: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
	HINT: Configure the DEFAULT_AUTO_FIELD setting or the AppConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
users.WritePermissions: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
	HINT: Configure the DEFAULT_AUTO_FIELD setting or the AppConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.

Those warnings are okay, you can ignore them...

Operations to perform: Synchronize unmigrated apps: editor, messages, metadata, staticfiles, structureeditor, viewer Apply all migrations: admin, auth, contenttypes, sessions, sites, users Synchronizing apps without migrations: Creating tables... Running deferred SQL... Running migrations: No migrations to apply.

This looks all good...

It keeps repeating this over and over again. I tried adding --env FLAT_DEBUG=1 but it changed nothing in the logs.

If you get this over and over again then uwsgi/flat is being restarted over and over again (because it keeps failing). There really aren't any more cues in the logs? Nothing else before it restarts?

I'm guessing (but not sure) it still has something to do with the UID and the docker setup. Maybe it has trouble switching to the uwsgi UID/GID 100 which is used by default. Try try setting UWSGI_UID and UWSGI_GID to 0 (root) in the environment variables passed to the containers and see if that helps (not ideal but okay). If it's not that then I wonder if it somehow fails to open a socket for 127.0.0.1:8888 due to the docker setup (is the loopback network device available inside the container?).

proycon avatar Mar 26 '25 12:03 proycon

There really aren't any more cues in the logs? Nothing else before it restarts? It complains about not being able to connect to the local database for a bit, but it eventually connects and proceeds. Here are the logs of it:

Starting FLAT via uwsgi...
*** Starting uWSGI 2.0.27 (64bit) on [Thu Mar 27 13:23:38 2025] ***
compiled with version: 14.2.0 on 22 October 2024 13:39:35
os: Linux-5.10.0-20-cloud-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13)
nodename: a0deb07eced8
machine: x86_64
clock source: unix
pcre jit disabled
detected number of CPU cores: 6
current working directory: /etc/service/uwsgi
detected binary path: /usr/sbin/uwsgi
group 0 not found.
Applying migrations...
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 289, in ensure_connection
    self.connect()
  File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 270, in connect
    self.connection = self.get_new_connection(conn_params)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/mysql/base.py", line 247, in get_new_connection
    connection = Database.connect(**conn_params)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/MySQLdb/__init__.py", line 121, in Connect
    return Connection(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/MySQLdb/connections.py", line 200, in __init__
    super().__init__(*args, **kwargs2)
MySQLdb.OperationalError: (2002, "Can't connect to local server through socket '/run/mysqld/mysqld.sock' (111)")

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/bin/django-admin", line 8, in <module>
    sys.exit(execute_from_command_line())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/usr/lib/python3.12/site-packages/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/lib/python3.12/site-packages/django/core/management/base.py", line 412, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/lib/python3.12/site-packages/django/core/management/base.py", line 458, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/core/management/base.py", line 106, in wrapper
    res = handle_func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/core/management/commands/migrate.py", line 100, in handle
    self.check(databases=[database])
  File "/usr/lib/python3.12/site-packages/django/core/management/base.py", line 485, in check
    all_issues = checks.run_checks(
                 ^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/core/checks/registry.py", line 88, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/core/checks/model_checks.py", line 36, in check_all_models
    errors.extend(model.check(**kwargs))
                  ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/models/base.py", line 1558, in check
    *cls._check_indexes(databases),
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/models/base.py", line 2002, in _check_indexes
    connection.features.supports_expression_indexes
  File "/usr/lib/python3.12/site-packages/django/utils/functional.py", line 57, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
                                         ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/mysql/features.py", line 331, in supports_expression_indexes
    not self.connection.mysql_is_mariadb
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/utils/functional.py", line 57, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
                                         ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/mysql/base.py", line 439, in mysql_is_mariadb
    return "mariadb" in self.mysql_server_info.lower()
                        ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/utils/functional.py", line 57, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
                                         ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/mysql/base.py", line 425, in mysql_server_info
    return self.mysql_server_data["version"]
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/utils/functional.py", line 57, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
                                         ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/mysql/base.py", line 399, in mysql_server_data
    with self.temporary_connection() as cursor:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/contextlib.py", line 137, in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 705, in temporary_connection
    with self.cursor() as cursor:
         ^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 330, in cursor
    return self._cursor()
           ^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 306, in _cursor
    self.ensure_connection()
  File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 288, in ensure_connection
    with self.wrap_database_errors:
         ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 289, in ensure_connection
    self.connect()
  File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/base/base.py", line 270, in connect
    self.connection = self.get_new_connection(conn_params)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/utils/asyncio.py", line 26, in inner
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/django/db/backends/mysql/base.py", line 247, in get_new_connection
    connection = Database.connect(**conn_params)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/MySQLdb/__init__.py", line 121, in Connect
    return Connection(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/MySQLdb/connections.py", line 200, in __init__
    super().__init__(*args, **kwargs2)
django.db.utils.OperationalError: (2002, "Can't connect to local server through socket '/run/mysqld/mysqld.sock' (111)")

However, after repeating this a few times, it eventually manages to connect to the database and the logs changes a bit. It stops crashing after trying to apply migration and becomes what I shared in my previous comment. It just keeps repeating it over and over again.

Try try setting UWSGI_UID and UWSGI_GID to 0 (root) in the environment variables passed to the containers and see if that helps (not ideal but okay)

This is already how I configured it, it's set up through the Dockerfile to rebuild the images with some other changes. I also made some changes to the /etc/subuid file so that Root inside the container becomes Parseme on the host:

parseme:1004:10000

Which seems to work fine because when I do ps aux | grep folia on the host I see :

Image

And inside the container :

Image

I thought it might be a permission issue somewhere, but wouldn't the logs show a permission denied in that case ?

Thank you for taking the time to help us with this.

ObadaS avatar Mar 27 '25 13:03 ObadaS