Skip to content

Commit a5f071e

Browse files
author
gitstart_bot
committed
chore: restrict users reporting crashes/coverage to only intended tools
1 parent 6312157 commit a5f071e

File tree

6 files changed

+530
-2
lines changed

6 files changed

+530
-2
lines changed

server/covmanager/views.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
from rest_framework import filters, mixins, viewsets
1313
from rest_framework.authentication import SessionAuthentication, TokenAuthentication
1414

15-
from crashmanager.models import Tool
15+
from crashmanager.models import Tool, User
1616
from server.views import JsonQueryFilterBackend, SimpleQueryFilterBackend
1717

1818
from .models import Collection, Report, ReportConfiguration, ReportSummary, Repository
1919
from .serializers import (
2020
CollectionSerializer,
2121
ReportConfigurationSerializer,
2222
ReportSerializer,
23-
RepositorySerializer,
23+
RepositorySerializer
2424
)
2525
from .SourceCodeProvider import SourceCodeProvider
2626
from .tasks import aggregate_coverage_data, calculate_report_summary
@@ -715,6 +715,25 @@ class CollectionViewSet(
715715
CollectionFilterBackend,
716716
]
717717

718+
def create(self, request, *args, **kwargs):
719+
"""Check user has access to tools before creation"""
720+
tools_str = request.data.get('tools')
721+
requested_tools = set(tools_str.split(','))
722+
723+
user = User.get_or_create_restricted(request.user)[0]
724+
if user.restricted:
725+
allowed_tools = set(user.defaultToolsFilter.values_list('name', flat=True))
726+
if not allowed_tools:
727+
raise PermissionDenied({"message": "No tools assigned to user"})
728+
729+
unauthorized_tools = requested_tools - allowed_tools
730+
if unauthorized_tools:
731+
raise PermissionDenied(
732+
{"message": f"You don't have permission to use the following tools: {', '.join(unauthorized_tools)}"}
733+
)
734+
735+
return super().create(request, *args, **kwargs)
736+
718737

719738
class ReportFilterBackend(filters.BaseFilterBackend):
720739
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from django.core.management.base import BaseCommand
2+
from rest_framework.authtoken.models import Token
3+
from crashmanager.models import Tool, User as CrashManagerUser
4+
5+
6+
class Command(BaseCommand):
7+
help = "Assigns a tool to a user's defaultToolsFilter by looking up their token and ensures the user is restricted."
8+
9+
def add_arguments(self, parser):
10+
parser.add_argument("token", help="The token string from get_auth_token command")
11+
parser.add_argument("tool_name", help="Name of the tool to add")
12+
13+
def handle(self, *args, **options):
14+
token_str = options["token"]
15+
tool_name = options["tool_name"]
16+
17+
try:
18+
token_obj = Token.objects.get(key=token_str)
19+
except Token.DoesNotExist:
20+
print(f"No token found for {token_str}")
21+
return
22+
23+
crash_manager_user = CrashManagerUser.get_or_create_restricted(token_obj.user)[0]
24+
25+
tool, created = Tool.objects.get_or_create(name=tool_name)
26+
if created:
27+
print(f"Tool '{tool_name}' was created.")
28+
29+
# Ensure the user is restricted, so they can only access the tool being added
30+
if not crash_manager_user.restricted:
31+
crash_manager_user.restricted = True
32+
crash_manager_user.save()
33+
print(f"User '{crash_manager_user.user.username}' has been restricted for security.")
34+
35+
crash_manager_user.defaultToolsFilter.add(tool)
36+
37+
print(f"Tool '{tool_name}' added to user '{crash_manager_user.user.username}'.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from django.core.management.base import BaseCommand
2+
from rest_framework.authtoken.models import Token
3+
from crashmanager.models import Tool, User as CrashManagerUser
4+
import sys
5+
6+
7+
class Command(BaseCommand):
8+
help = "Removes a tool from a user's defaultToolsFilter by looking up their token."
9+
10+
def add_arguments(self, parser):
11+
parser.add_argument("token", help="The token string from get_auth_token command")
12+
parser.add_argument("tool_name", help="Name of the tool to remove")
13+
14+
def handle(self, *args, **options):
15+
token_str = options["token"]
16+
tool_name = options["tool_name"]
17+
18+
try:
19+
token_obj = Token.objects.get(key=token_str)
20+
except Token.DoesNotExist:
21+
print(f"No token found for {token_str}")
22+
return
23+
24+
crash_manager_user = CrashManagerUser.get_or_create_restricted(token_obj.user)[0]
25+
26+
try:
27+
tool = Tool.objects.get(name=tool_name)
28+
except Tool.DoesNotExist:
29+
print(f"Error: Tool '{tool_name}' is not present in the database")
30+
return
31+
32+
crash_manager_user.defaultToolsFilter.remove(tool)
33+
34+
# Keep user restricted regardless of tool count
35+
if not crash_manager_user.restricted:
36+
crash_manager_user.restricted = True
37+
crash_manager_user.save()
38+
if not crash_manager_user.defaultToolsFilter.exists():
39+
print(f"User '{crash_manager_user.user.username}' has no tools assigned but remains restricted.")
40+
41+
print(f"Tool '{tool_name}' removed from user '{crash_manager_user.user.username}'.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Tests for add/remove tool management commands
2+
3+
@author: GitStart
4+
5+
@license:
6+
This Source Code Form is subject to the terms of the Mozilla Public
7+
License, v. 2.0. If a copy of the MPL was not distributed with this
8+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
9+
"""
10+
import pytest
11+
from django.core.management import CommandError, call_command
12+
from django.contrib.auth.models import User
13+
from rest_framework.authtoken.models import Token
14+
from crashmanager.models import Tool, User as CrashManagerUser
15+
16+
pytestmark = pytest.mark.django_db()
17+
18+
def test_add_tool_to_token_new_tool(capsys):
19+
"""Test adding new tool to user's filter"""
20+
user = User.objects.create_user("testuser")
21+
token = Token.objects.create(user=user)
22+
23+
call_command("add_tool_to_token", token.key, "newtool")
24+
25+
# Check command output
26+
out, _ = capsys.readouterr()
27+
assert "Tool 'newtool' was created." in out
28+
assert "added to user" in out
29+
30+
# Verify database state
31+
cm_user = CrashManagerUser.objects.get(user=user)
32+
assert cm_user.restricted is True
33+
assert cm_user.defaultToolsFilter.filter(name="newtool").exists()
34+
35+
def test_add_tool_to_token_existing_tool(capsys):
36+
"""Test adding existing tool doesn't recreate it"""
37+
# Create tool first
38+
Tool.objects.create(name="existingtool")
39+
40+
user = User.objects.create_user("testuser")
41+
token = Token.objects.create(user=user)
42+
43+
call_command("add_tool_to_token", token.key, "existingtool")
44+
45+
# Check command output
46+
out, _ = capsys.readouterr()
47+
assert "Tool 'existingtool' was created." not in out
48+
assert "added to user" in out
49+
50+
def test_add_tool_to_token_restricts_user(capsys):
51+
"""Test unrestricted user becomes restricted when adding tool"""
52+
user = User.objects.create_user("testuser")
53+
cm_user = CrashManagerUser.get_or_create_restricted(user)[0]
54+
cm_user.restricted = False
55+
cm_user.save()
56+
57+
token = Token.objects.create(user=user)
58+
59+
call_command("add_tool_to_token", token.key, "newtool")
60+
61+
# Check warning message
62+
out, _ = capsys.readouterr()
63+
assert "has been restricted for security" in out
64+
65+
# Verify restriction
66+
cm_user.refresh_from_db()
67+
assert cm_user.restricted is True
68+
69+
def test_remove_tool_from_token_exists(capsys):
70+
"""Test removing existing tool from filter"""
71+
user = User.objects.create_user("testuser")
72+
cm_user = CrashManagerUser.get_or_create_restricted(user)[0]
73+
tool = Tool.objects.create(name="oldtool")
74+
cm_user.defaultToolsFilter.add(tool)
75+
76+
token = Token.objects.create(user=user)
77+
78+
call_command("remove_tool_from_token", token.key, "oldtool")
79+
80+
# Check output
81+
out, _ = capsys.readouterr()
82+
assert "removed from user" in out
83+
assert not cm_user.defaultToolsFilter.filter(name="oldtool").exists()
84+
85+
def test_remove_tool_from_token_last_tool(capsys):
86+
"""Test warning when removing last tool"""
87+
user = User.objects.create_user("testuser")
88+
cm_user = CrashManagerUser.get_or_create_restricted(user)[0]
89+
tool = Tool.objects.create(name="lasttool")
90+
cm_user.defaultToolsFilter.add(tool)
91+
92+
token = Token.objects.create(user=user)
93+
94+
call_command("remove_tool_from_token", token.key, "lasttool")
95+
96+
# Check warning
97+
out, _ = capsys.readouterr()
98+
assert "has no tools assigned" in out
99+
100+
# Refresh from DB to get updated restriction status
101+
cm_user.refresh_from_db()
102+
assert cm_user.restricted is True
103+
104+
def test_remove_tool_from_token_nonexistent(capsys):
105+
"""Test removing non-existent tool shows error"""
106+
user = User.objects.create_user("testuser")
107+
token = Token.objects.create(user=user)
108+
109+
# Should return normally with error message
110+
call_command("remove_tool_from_token", token.key, "notexist")
111+
112+
# Verify error message
113+
out, _ = capsys.readouterr()
114+
assert "Error: Tool 'notexist' is not present in the database" in out
115+
116+
# Verify no changes to tools
117+
cm_user = CrashManagerUser.objects.get(user=user)
118+
assert cm_user.defaultToolsFilter.count() == 0
119+
120+
def test_add_tool_to_token_invalid_token(capsys):
121+
"""Test error handling for invalid token"""
122+
call_command("add_tool_to_token", "invalid", "tool")
123+
out, _ = capsys.readouterr()
124+
assert "No token found for invalid" in out
125+
126+
def test_remove_tool_from_token_invalid_token(capsys):
127+
"""Test error handling for invalid token"""
128+
call_command("remove_tool_from_token", "invalid", "tool")
129+
out, _ = capsys.readouterr()
130+
assert "No token found for invalid" in out

0 commit comments

Comments
 (0)