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

REST API to recalculate bits and clone katalogus settings #3572

Merged
merged 7 commits into from
Oct 1, 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
4 changes: 4 additions & 0 deletions rocky/katalogus/views/katalogus_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, TemplateView
from httpx import HTTPError
from structlog import get_logger
from tools.models import Organization

from katalogus.client import get_katalogus

logger = get_logger(__name__)


class ConfirmCloneSettingsView(
OrganizationPermissionRequiredMixin,
Expand All @@ -35,6 +38,7 @@ def get_context_data(self, **kwargs):

def post(self, request, *args, **kwargs):
to_organization = Organization.objects.get(code=kwargs["to_organization"])
logger.info("Cloning organization settings", event_code=910000, to_organization_code=to_organization.code)
get_katalogus(self.organization.code).clone_all_configuration_to_organization(to_organization.code)
messages.add_message(
self.request,
Expand Down
4 changes: 4 additions & 0 deletions rocky/rocky/views/organization_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView
from structlog import get_logger
from tools.models import Organization
from tools.view_helpers import OrganizationBreadcrumbsMixin

from octopoes.connector.octopoes import OctopoesAPIConnector

logger = get_logger(__name__)


class OrganizationListView(
OrganizationBreadcrumbsMixin,
Expand All @@ -40,6 +43,7 @@ def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
start_time = datetime.now()
for organization in organizations:
try:
logger.info("Recalculating bits", event_code=920000, organization_code=organization.code)
number_of_bits += OctopoesAPIConnector(settings.OCTOPOES_API, organization.code).recalculate_bits()
except Exception as exc:
failed.append(f"{organization}, ({str(exc)})")
Expand Down
4 changes: 4 additions & 0 deletions rocky/rocky/views/organization_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from structlog import get_logger
from tools.view_helpers import OrganizationDetailBreadcrumbsMixin

logger = get_logger(__name__)


class PageActions(Enum):
RECALCULATE = "recalculate"
Expand All @@ -26,6 +29,7 @@ def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
if not self.request.user.has_perm("tools.can_recalculate_bits"):
raise PermissionDenied()
if action == PageActions.RECALCULATE.value:
logger.info("Recalculating bits", event_code=920000)
connector = self.octopoes_api_connector

start_time = datetime.now()
Expand Down
55 changes: 55 additions & 0 deletions rocky/tests/test_api_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Returns200,
Returns201,
Returns204,
Returns400,
Returns403,
Returns409,
Returns500,
Expand Down Expand Up @@ -363,3 +364,57 @@ class TestIndemnificationAlreadyExists(APIViewTest, UsesPostMethod, Returns409):
def test_it_returns_indemnification(self, json, superuser_member):
expected = {"indemnification": True, "user": superuser_member.user.id}
assert json == expected


class TestRecalculateBits(APIViewTest, UsesPostMethod, Returns200):
url = lambda_fixture(lambda organization: reverse("organization-recalculate-bits", args=[organization.pk]))

@pytest.fixture
def client(self, drf_redteam_client, redteamuser):
redteamuser.user_permissions.set([Permission.objects.get(codename="can_recalculate_bits")])
return drf_redteam_client

@pytest.fixture(autouse=True)
def mock_octopoes(self, mocker):
return mocker.patch("tools.viewsets.OctopoesAPIConnector.recalculate_bits", return_value=42)

def test_it_recalculates_bits(self, json):
expected = {"number_of_bits": 42}
assert json == expected


class TestRecalculateBitsNoPermission(APIViewTest, UsesPostMethod, Returns403):
url = lambda_fixture(lambda organization: reverse("organization-recalculate-bits", args=[organization.pk]))
client = lambda_fixture("drf_redteam_client")


class TestKatalogusCloneSettings(APIViewTest, UsesPostMethod, Returns200):
url = lambda_fixture(lambda organization: reverse("organization-clone-katalogus-settings", args=[organization.pk]))
data = lambda_fixture(lambda organization_b: {"to_organization": organization_b.id})

@pytest.fixture
def client(self, drf_redteam_client, redteamuser):
redteamuser.user_permissions.set([Permission.objects.get(codename="can_set_katalogus_settings")])
return drf_redteam_client

@pytest.fixture(autouse=True)
def mock_katalogus(self, mocker):
return mocker.patch("katalogus.client.KATalogusClientV1")

def test_it_clones_settings(self, mock_katalogus, organization_b):
mock_katalogus().clone_all_configuration_to_organization.assert_called_once_with(organization_b.code)


class TestCloneKatalogusSettingsNoPermission(APIViewTest, UsesPostMethod, Returns403):
url = lambda_fixture(lambda organization: reverse("organization-clone-katalogus-settings", args=[organization.pk]))
client = lambda_fixture("drf_redteam_client")


class TestCloneKatalogusSettingsInvalidData(APIViewTest, UsesPostMethod, Returns400):
url = lambda_fixture(lambda organization: reverse("organization-clone-katalogus-settings", args=[organization.pk]))
data = lambda_fixture(lambda organization_b: {"wrong_field": organization_b.id})

@pytest.fixture
def client(self, drf_redteam_client, redteamuser):
redteamuser.user_permissions.set([Permission.objects.get(codename="can_set_katalogus_settings")])
return drf_redteam_client
13 changes: 13 additions & 0 deletions rocky/tools/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from rest_framework.permissions import BasePermission


# This is a bit clunky, but DRF doesn't allow you to specify a permission
# directly, only a Permission class
class CanRecalculateBits(BasePermission):
def has_permission(self, request, view) -> bool:
return request.user.has_perm("tools.can_recalculate_bits")


class CanSetKatalogusSettings(BasePermission):
def has_permission(self, request, view) -> bool:
return request.user.has_perm("tools.can_set_katalogus_settings")
5 changes: 5 additions & 0 deletions rocky/tools/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from rest_framework.serializers import PrimaryKeyRelatedField, Serializer
from tagulous.contrib.drf import TagSerializer

from tools.models import Organization
Expand All @@ -14,3 +15,7 @@ class Meta:
model = Organization
fields = ["id", "name", "code", "tags"]
read_only_fields = ["code"]


class ToOrganizationSerializer(Serializer):
to_organization = PrimaryKeyRelatedField(queryset=Organization.objects.all())
37 changes: 36 additions & 1 deletion rocky/tools/viewsets.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from django.conf import settings
from katalogus.client import get_katalogus
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from structlog import get_logger

from octopoes.connector.octopoes import OctopoesAPIConnector
from tools.models import Indemnification, Organization
from tools.serializers import OrganizationSerializer, OrganizationSerializerReadOnlyCode
from tools.permissions import CanRecalculateBits, CanSetKatalogusSettings
from tools.serializers import OrganizationSerializer, OrganizationSerializerReadOnlyCode, ToOrganizationSerializer

logger = get_logger(__name__)


class OrganizationViewSet(viewsets.ModelViewSet):
Expand Down Expand Up @@ -50,3 +57,31 @@ def set_indemnification(self, request, pk=None):
indemnification = Indemnification.objects.create(organization=organization, user=self.request.user)

return Response({"indemnification": True, "user": indemnification.user.pk}, status=status.HTTP_201_CREATED)

@action(detail=True, methods=["post"], permission_classes=[CanRecalculateBits])
def recalculate_bits(self, request, pk=None):
organization = self.get_object()
logger.info("Recalculating bits", event_code=920000, organization_code=organization.code)
connector = OctopoesAPIConnector(settings.OCTOPOES_API, organization.code)
number_of_bits = connector.recalculate_bits()

underdarknl marked this conversation as resolved.
Show resolved Hide resolved
return Response({"number_of_bits": number_of_bits})

@action(detail=True, methods=["post"], permission_classes=[CanSetKatalogusSettings])
def clone_katalogus_settings(self, request, pk=None):
from_organization = self.get_object()

serializer = ToOrganizationSerializer(data=request.data)
if serializer.is_valid():
to_organization = serializer.validated_data["to_organization"]
logger.info(
"Cloning organization settings",
event_code=910000,
organization_code=from_organization.code,
to_organization_code=to_organization.code,
)
get_katalogus(from_organization.code).clone_all_configuration_to_organization(to_organization.code)

underdarknl marked this conversation as resolved.
Show resolved Hide resolved
return Response()
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)