Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add is_voting_rule #1076

Merged
merged 1 commit into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions server/static_src/scss/user_portal.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
:root {
--main-color: #040203;
--background-color: #fafafa;
--danger-color: #ea5165;
--default-weight: 400;
--bold-weight: 700;
--btn-border: 2px;
Expand Down Expand Up @@ -29,6 +30,10 @@ dd {
margin: 0 0 .5rem 0;
}

p.danger {
color: var(--danger-color);
}

/* Grid */

.main-head {
Expand Down
26 changes: 25 additions & 1 deletion tests/santa/test_api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from zentral.contrib.santa.models import Configuration, Rule, RuleSet, Target, Enrollment
from zentral.core.events.base import AuditEvent
from zentral.utils.payloads import get_payload_identifier
from .utils import new_cdhash, new_sha256, new_signing_id_identifier, new_team_id
from .utils import force_rule, new_cdhash, new_sha256, new_signing_id_identifier, new_team_id


class APIViewsTestCase(TestCase):
Expand Down Expand Up @@ -1047,6 +1047,7 @@ def test_rule_create(self, post_event):
"excluded_serial_numbers": [get_random_string(12)],
"tags": [t.id for t in self.force_tags(1)],
"excluded_tags": [t.id for t in self.force_tags(1)],
"is_voting_rule": True, # Read Only!!!
}
with self.captureOnCommitCallbacks(execute=True):
response = self.post_json_data(reverse("santa_api:rules"), data)
Expand All @@ -1062,6 +1063,7 @@ def test_rule_create(self, post_event):
"description": "Description",
"custom_msg": '',
"ruleset": None,
"is_voting_rule": False,
"primary_users": data["primary_users"],
"excluded_primary_users": data["excluded_primary_users"],
"serial_numbers": data["serial_numbers"],
Expand All @@ -1081,6 +1083,7 @@ def test_rule_create(self, post_event):
"description": rule.description,
"primary_users": rule.primary_users,
"ruleset": None,
"is_voting_rule": rule.is_voting_rule,
"custom_msg": '',
"excluded_primary_users": rule.excluded_primary_users,
"serial_numbers": rule.serial_numbers,
Expand Down Expand Up @@ -1629,6 +1632,19 @@ def test_update_rule_unauthorized(self):
response = self.put_json_data(reverse("santa_api:rule", args=(rule.pk,)), {}, include_token=False)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_update_rule_voting_rule_error(self):
self.set_permissions("santa.change_rule")
rule = force_rule(is_voting_rule=True)
response = self.put_json_data(
reverse("santa_api:rule", args=(rule.pk,)),
{"configuration": rule.configuration.pk,
"policy": rule.policy,
"target_identifier": rule.target.identifier,
"target_type": rule.target.type}
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), ['A voting rule cannot be directly updated'])

@patch("zentral.core.queues.backends.kombu.EventQueues.post_event")
def test_update_rule(self, post_event):
configuration = self.force_configuration()
Expand Down Expand Up @@ -1662,6 +1678,7 @@ def test_update_rule(self, post_event):
"description": "new description",
"custom_msg": "new custom block message",
"ruleset": None,
"is_voting_rule": False,
"primary_users": data["primary_users"],
"excluded_primary_users": data["excluded_primary_users"],
"serial_numbers": data["serial_numbers"],
Expand Down Expand Up @@ -1779,6 +1796,13 @@ def test_rule_delete_permission_denied(self):
response = self.delete(reverse("santa_api:rule", args=(rule.pk,)))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_rule_delete_voting_rule_error(self):
self.set_permissions("santa.delete_rule")
rule = force_rule(is_voting_rule=True)
response = self.delete(reverse("santa_api:rule", args=(rule.pk,)))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), ['A voting rule cannot be directly deleted'])

# list configuration

def test_get_configurations_unauthorized(self):
Expand Down
115 changes: 115 additions & 0 deletions tests/santa/test_ballot_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,108 @@ def test_ballot_box_existing_votes_reset_no_existing_votes(self):
set()
)

def test_ballot_box_no_conflicting_rule(self):
realm, realm_user = force_realm_user()
force_configuration(voting_realm=realm)
ballot_box = BallotBox.for_realm_user(self.file_target, realm_user, all_configurations=True)
self.assertEqual(ballot_box.conflicting_non_voting_rules, {})
self.assertEqual(ballot_box.conflicting_non_voting_rule_custom_messages, [])

def test_ballot_box_cdhash_conflicting_rule(self):
realm, realm_user = force_realm_user()
configuration = force_configuration(voting_realm=realm)
ballot_box = BallotBox.for_realm_user(self.file_target, realm_user, all_configurations=True)
rule = Rule.objects.create(
configuration=configuration,
policy=Rule.Policy.BLOCKLIST,
target=self.cdhash_target
)
ballot_box.conflicting_non_voting_rules
self.assertEqual(
ballot_box.conflicting_non_voting_rules,
{configuration: [rule]}
)
self.assertEqual(ballot_box.conflicting_non_voting_rule_custom_messages,
["Voting is not allowed on this app."])

def test_ballot_box_binary_conflicting_rule(self):
realm, realm_user = force_realm_user()
configuration = force_configuration(voting_realm=realm)
ballot_box = BallotBox.for_realm_user(self.cdhash_target, realm_user, all_configurations=True)
rule = Rule.objects.create(
configuration=configuration,
policy=Rule.Policy.BLOCKLIST,
target=self.file_target,
)
ballot_box.conflicting_non_voting_rules
self.assertEqual(
ballot_box.conflicting_non_voting_rules,
{configuration: [rule]}
)

def test_ballot_box_signing_id_conflicting_rule(self):
realm, realm_user = force_realm_user()
configuration = force_configuration(voting_realm=realm)
ballot_box = BallotBox.for_realm_user(self.metabundle_target, realm_user, all_configurations=True)
Rule.objects.create( # not a conflict, because more precise
configuration=configuration,
policy=Rule.Policy.BLOCKLIST,
target=self.file_target
)
rule = Rule.objects.create( # conflict
configuration=configuration,
policy=Rule.Policy.BLOCKLIST,
target=self.signing_id_target,
custom_msg="YOLO FOMO",
)
ballot_box.conflicting_non_voting_rules
self.assertEqual(
ballot_box.conflicting_non_voting_rules,
{configuration: [rule]}
)
self.assertEqual(ballot_box.conflicting_non_voting_rule_custom_messages,
["YOLO FOMO"])

def test_ballot_box_certificate_conflicting_rule(self):
realm, realm_user = force_realm_user()
configuration = force_configuration(voting_realm=realm)
ballot_box = BallotBox.for_realm_user(self.cert_target, realm_user, all_configurations=True)
Rule.objects.create( # not a conflict, because more precise
configuration=configuration,
policy=Rule.Policy.BLOCKLIST,
target=self.signing_id_target,
)
rule = Rule.objects.create( # conflict
configuration=configuration,
policy=Rule.Policy.BLOCKLIST,
target=self.team_id_target,
)
ballot_box.conflicting_non_voting_rules
self.assertEqual(
ballot_box.conflicting_non_voting_rules,
{configuration: [rule]}
)

def test_ballot_box_team_id_conflicting_rule(self):
realm, realm_user = force_realm_user()
configuration = force_configuration(voting_realm=realm)
ballot_box = BallotBox.for_realm_user(self.team_id_target, realm_user, all_configurations=True)
Rule.objects.create( # not a conflict, because more precise
configuration=configuration,
policy=Rule.Policy.BLOCKLIST,
target=self.cert_target,
)
rule = Rule.objects.create( # conflict
configuration=configuration,
policy=Rule.Policy.BLOCKLIST,
target=self.team_id_target,
)
ballot_box.conflicting_non_voting_rules
self.assertEqual(
ballot_box.conflicting_non_voting_rules,
{configuration: [rule]}
)

def test_ballot_box_check_voting_allowed_for_configuration_anonymous_voter(self):
ballot_box = BallotBox.for_realm_user(self.file_target, None)
self.assertEqual(ballot_box.check_voting_allowed_for_configuration(None, True),
Expand Down Expand Up @@ -393,6 +495,19 @@ def test_ballot_box_check_voting_allowed_for_configuration_downvote_bundle_error
self.assertEqual(ballot_box.check_voting_allowed_for_configuration(configuration, False),
"A BUNDLE cannot be downvoted")

def test_ballot_box_check_voting_allowed_for_configuration_conflicting_rule(self):
realm, realm_user = force_realm_user()
configuration = force_configuration(voting_realm=realm, default_ballot_target_types=[Target.Type.BUNDLE])
Rule.objects.create(
configuration=configuration,
target=self.file_target,
policy=Rule.Policy.BLOCKLIST,
custom_msg=get_random_string(12)
)
ballot_box = BallotBox.for_realm_user(self.bundle_target, realm_user, all_configurations=True)
self.assertEqual(ballot_box.check_voting_allowed_for_configuration(configuration, True),
"Conflicting non-voting rule")

def test_ballot_box_get_default_votes(self):
realm, realm_user = force_realm_user()
configuration = force_configuration(voting_realm=realm, default_ballot_target_types=[Target.Type.BUNDLE])
Expand Down
66 changes: 65 additions & 1 deletion tests/santa/test_setup_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from zentral.contrib.santa.models import Bundle, Enrollment, Rule, Target
from zentral.core.events.base import AuditEvent
from .utils import (force_configuration,
force_realm, force_realm_group, force_realm_user, force_voting_group,
force_realm, force_realm_group, force_realm_user, force_rule, force_voting_group,
new_cdhash, new_sha256, new_signing_id_identifier, new_team_id)


Expand Down Expand Up @@ -810,6 +810,58 @@ def test_configuration_rules_binary_search(self):
self.assertNotContains(response, reverse("santa:configuration_rules",
args=(rule.configuration.pk,)) + '">all the items')

def test_configuration_rules_no_links(self):
rule = self._force_rule(target_type=Target.Type.BINARY)
voting_rule = force_rule(configuration=rule.configuration, is_voting_rule=True)
self._login("santa.view_rule")
response = self.client.get(reverse("santa:configuration_rules", args=(rule.configuration.pk,)))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "santa/configuration_rules.html")
self.assertContains(response, rule.target.identifier)
self.assertContains(response, voting_rule.target.identifier)
self.assertNotContains(
response,
reverse("santa:update_configuration_rule", args=(rule.configuration.pk, rule.pk))
)
self.assertNotContains(
response,
reverse("santa:delete_configuration_rule", args=(rule.configuration.pk, rule.pk))
)
self.assertNotContains(
response,
reverse("santa:update_configuration_rule", args=(voting_rule.configuration.pk, voting_rule.pk))
)
self.assertNotContains(
response,
reverse("santa:delete_configuration_rule", args=(voting_rule.configuration.pk, voting_rule.pk))
)

def test_configuration_rules_links(self):
rule = self._force_rule(target_type=Target.Type.BINARY)
voting_rule = force_rule(configuration=rule.configuration, is_voting_rule=True)
self._login("santa.change_rule", "santa.delete_rule", "santa.view_rule")
response = self.client.get(reverse("santa:configuration_rules", args=(rule.configuration.pk,)))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "santa/configuration_rules.html")
self.assertContains(response, rule.target.identifier)
self.assertContains(response, voting_rule.target.identifier)
self.assertContains(
response,
reverse("santa:update_configuration_rule", args=(rule.configuration.pk, rule.pk))
)
self.assertContains(
response,
reverse("santa:delete_configuration_rule", args=(rule.configuration.pk, rule.pk))
)
self.assertNotContains(
response,
reverse("santa:update_configuration_rule", args=(voting_rule.configuration.pk, voting_rule.pk))
)
self.assertNotContains(
response,
reverse("santa:delete_configuration_rule", args=(voting_rule.configuration.pk, voting_rule.pk))
)

def test_configuration_rules_no_result(self):
rule = self._force_rule(target_type=Target.Type.BINARY)
self._login("santa.view_rule")
Expand Down Expand Up @@ -1232,6 +1284,12 @@ def test_update_configuration_rule_scope_conflict(self):
'excluded_primary_users': ["'fomo' both included and excluded"],
'excluded_tags': [f"'{tags[0].name}' both included and excluded"]})

def test_update_configuration_rule_voting_rule_error(self):
self._login("santa.change_rule")
rule = force_rule(is_voting_rule=True)
response = self.client.get(reverse("santa:update_configuration_rule", args=(rule.configuration.pk, rule.pk)))
self.assertEqual(response.status_code, 404)

# delete configuration rule

def test_delete_configuration_rule(self):
Expand Down Expand Up @@ -1259,6 +1317,12 @@ def test_delete_configuration_rule(self):
self.assertTemplateUsed(response, "santa/configuration_rules.html")
self.assertFalse(any(rule.target.identifier == binary_hash for rule in response.context["object_list"]))

def test_delete_configuration_rule_voting_rule_error(self):
self._login("santa.delete_rule")
rule = force_rule(is_voting_rule=True)
response = self.client.get(reverse("santa:delete_configuration_rule", args=(rule.configuration.pk, rule.pk)))
self.assertEqual(response.status_code, 404)

# pick rule binary

def test_pick_rule_binary_redirect(self):
Expand Down
23 changes: 22 additions & 1 deletion tests/santa/test_up_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
from django.conf import settings
from django.http import HttpRequest
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.test import TestCase, override_settings
from realms.backends.views import finalize_session
from realms.models import RealmAuthenticationSession
from zentral.contrib.santa.ballot_box import BallotBox
from zentral.contrib.santa.events import SantaBallotEvent, SantaTargetStateUpdateEvent
from zentral.contrib.santa.models import Ballot, Target
from zentral.contrib.santa.models import Ballot, Rule, Target
from .utils import add_file_to_test_class, force_configuration, force_enrolled_machine, force_realm, force_realm_user


Expand Down Expand Up @@ -321,3 +322,23 @@ def test_target_post_file_signing_id_ballot_box_yes_duplicate(self):
response.context["existing_votes"],
[(self.configuration, True)],
)

def test_target_get_conflicting_rule_ballot_box(self):
self._login()
rule = Rule.objects.create( # conflicting rule
configuration=self.configuration,
policy=Rule.Policy.BLOCKLIST,
target=self.team_id_target,
custom_msg=get_random_string(12),
)
response = self.client.get(
reverse("realms_public:santa_up:target",
args=(self.realm.pk, "bundle", self.bundle_sha256))
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "user_portal/santa_target_detail.html")
self.assertNotContains(response, "Vote to allowlist")
self.assertContains(response, rule.custom_msg)
ballot_box = response.context["ballot_box"]
self.assertEqual(ballot_box.target.type, Target.Type.METABUNDLE)
self.assertEqual(ballot_box.target.identifier, self.metabundle_sha256)
12 changes: 11 additions & 1 deletion tests/santa/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,17 @@ def force_rule(
target_identifier=None,
configuration=None,
policy=Rule.Policy.BLOCKLIST,
is_voting_rule=False,
):
target = force_target(target_type, target_identifier)
if configuration is None:
configuration = force_configuration()
return Rule.objects.create(configuration=configuration, target=target, policy=policy)
return Rule.objects.create(
configuration=configuration,
target=target,
policy=policy,
is_voting_rule=is_voting_rule
)


# file
Expand Down Expand Up @@ -287,12 +293,16 @@ def add_file_to_test_class(cls):
uploaded_bundles = _create_bundle_binaries(events)
_commit_files(events)
update_metabundles(uploaded_bundles)
cls.cdhash_target = Target.objects.get(type=Target.Type.CDHASH, identifier=cls.cdhash)
cls.file_target = Target.objects.get(type=Target.Type.BINARY, identifier=cls.file_sha256)
cls.file = File.objects.get(sha_256=cls.file_sha256)
cls.bundle_target = Target.objects.get(type=Target.Type.BUNDLE, identifier=cls.bundle_sha256)
cls.bundle = cls.bundle_target.bundle
cls.metabundle_target = cls.bundle.metabundle.target
cls.metabundle_sha256 = cls.bundle.metabundle.target.identifier
cls.cert_target = Target.objects.get(type=Target.Type.CERTIFICATE, identifier=cls.file_cert_sha256)
cls.signing_id_target = Target.objects.get(type=Target.Type.SIGNING_ID, identifier=cls.file_signing_id)
cls.team_id_target = Target.objects.get(type=Target.Type.TEAM_ID, identifier=cls.file_team_id)


# ballot
Expand Down
Loading
Loading