gitweb: Don't use privileged action feature to run as different user

- Instead implement running specific commands inside the privileged action as a
specific user.

Tests:

- Gitweb functional tests and unit tests work.

- Running various operations such as clone, create, set branch, rename, etc. all
result in repositories (and all their contents) owned by www-data:www-data.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net>
This commit is contained in:
Sunil Mohan Adapa 2025-08-11 14:10:20 -07:00 committed by Joseph Nuthalapati
parent 7a4fb9d9f6
commit 87331e7c97
No known key found for this signature in database
GPG Key ID: 5398F00A2FA43C35
5 changed files with 31 additions and 18 deletions

View File

@ -6,6 +6,7 @@ pytest configuration for all tests.
import importlib import importlib
import os import os
import pathlib import pathlib
import subprocess
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@ -191,6 +192,18 @@ def fixture_mock_privileged(request):
module.__dict__[name] = wrapper module.__dict__[name] = wrapper
@pytest.fixture(name='mock_run_as_user')
def fixture_mock_run_as_user():
"""A fixture to override action_utils.run_as_user."""
def _bypass_runuser(*args, username, **kwargs):
return subprocess.run(*args, **kwargs)
with patch('plinth.action_utils.run_as_user') as mock:
mock.side_effect = _bypass_runuser
yield
@pytest.fixture(name='splinter_screenshot_dir', scope='session') @pytest.fixture(name='splinter_screenshot_dir', scope='session')
def fixture_splinter_screenshot_dir(request): def fixture_splinter_screenshot_dir(request):
"""Set default screenshot directory to ./screenshots. """Set default screenshot directory to ./screenshots.

View File

@ -14,7 +14,7 @@ from plinth.package import Packages
from . import manifest, privileged from . import manifest, privileged
from .forms import is_repo_url from .forms import is_repo_url
from .manifest import GIT_REPO_PATH, REPO_DIR_OWNER from .manifest import GIT_REPO_PATH
_description = [ _description = [
_('Git is a distributed version-control system for tracking changes in ' _('Git is a distributed version-control system for tracking changes in '
@ -217,7 +217,7 @@ def get_repo_list():
def repo_info(repo): def repo_info(repo):
"""Get information about repository.""" """Get information about repository."""
info = privileged.repo_info(repo, _run_as_user=REPO_DIR_OWNER) info = privileged.repo_info(repo)
if info['access'] == 'private': if info['access'] == 'private':
info['is_private'] = True info['is_private'] = True
else: else:
@ -248,5 +248,4 @@ def edit_repo(form_initial, form_cleaned):
privileged.set_repo_access(repo, 'public') privileged.set_repo_access(repo, 'public')
if form_cleaned['default_branch'] != form_initial['default_branch']: if form_cleaned['default_branch'] != form_initial['default_branch']:
privileged.set_default_branch(repo, form_cleaned['default_branch'], privileged.set_default_branch(repo, form_cleaned['default_branch'])
_run_as_user=REPO_DIR_OWNER)

View File

@ -14,12 +14,11 @@ from django.utils.translation import gettext_lazy as _
from plinth.modules import gitweb from plinth.modules import gitweb
from . import privileged from . import privileged
from .manifest import REPO_DIR_OWNER
def _get_branches(repo): def _get_branches(repo):
"""Get all the branches in the repository.""" """Get all the branches in the repository."""
branch_data = privileged.get_branches(repo, _run_as_user=REPO_DIR_OWNER) branch_data = privileged.get_branches(repo)
default_branch = branch_data['default_branch'] default_branch = branch_data['default_branch']
branches = branch_data['branches'] branches = branch_data['branches']

View File

@ -199,9 +199,11 @@ def _get_default_branch(repo):
"""Get default branch of the repository.""" """Get default branch of the repository."""
repo_path = GIT_REPO_PATH / repo repo_path = GIT_REPO_PATH / repo
return subprocess.check_output( return action_utils.run_as_user(
['git', '-C', ['git', '-C',
str(repo_path), 'symbolic-ref', '--short', 'HEAD']).decode().strip() str(repo_path), 'symbolic-ref', '--short', 'HEAD'],
username=REPO_DIR_OWNER, check=True,
stdout=subprocess.PIPE).stdout.decode().strip()
def _get_repo_description(repo): def _get_repo_description(repo):
@ -267,11 +269,12 @@ def _set_access_status(repo, status):
def _get_branches(repo): def _get_branches(repo):
"""Return list of the branches in the repository.""" """Return list of the branches in the repository."""
output = subprocess.check_output( process = action_utils.run_as_user(
['git', '-C', repo, 'branch', '--format=%(refname:short)'], ['git', '-C', repo, 'branch', '--format=%(refname:short)'],
cwd=GIT_REPO_PATH) cwd=GIT_REPO_PATH, username=REPO_DIR_OWNER, check=True,
stdout=subprocess.PIPE)
return output.decode().strip().split() return process.stdout.decode().strip().split()
@privileged @privileged
@ -299,10 +302,9 @@ def set_default_branch(name: str, branch: str):
if branch not in _get_branches(repo): if branch not in _get_branches(repo):
raise ValueError('No such branch') raise ValueError('No such branch')
subprocess.check_call([ action_utils.run_as_user(
'git', '-C', repo, 'symbolic-ref', 'HEAD', ['git', '-C', repo, 'symbolic-ref', 'HEAD', f'refs/heads/{branch}'],
"refs/heads/{}".format(branch) cwd=GIT_REPO_PATH, check=True, username=REPO_DIR_OWNER)
], cwd=GIT_REPO_PATH)
@privileged @privileged

View File

@ -16,16 +16,16 @@ REPO_DATA = {
'access': 'private', 'access': 'private',
} }
pytestmark = pytest.mark.usefixtures('mock_privileged') pytestmark = pytest.mark.usefixtures('mock_privileged', 'mock_run_as_user')
privileged_modules_to_mock = ['plinth.modules.gitweb.privileged'] privileged_modules_to_mock = ['plinth.modules.gitweb.privileged']
git_installed = pytest.mark.skipif(not pathlib.Path('/usr/bin/git').exists(), git_installed = pytest.mark.skipif(not pathlib.Path('/usr/bin/git').exists(),
reason='git is not installed') reason='git is not installed')
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def fixture_set_repo_path(tmpdir): def fixture_set_repo_path(tmp_path):
"""Set a repository path in the actions module.""" """Set a repository path in the actions module."""
privileged.GIT_REPO_PATH = tmpdir privileged.GIT_REPO_PATH = tmp_path
@pytest.fixture(name='existing_repo') @pytest.fixture(name='existing_repo')