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

Make care single source of truth for asset config on middleware #2000

Merged
merged 30 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a2e9ac2
add routes for middleware to fetch asset config
sainak Mar 21, 2024
c3b47fe
cache middleware jwks
sainak Mar 21, 2024
b6b386b
allow asset authentication for assetbed
sainak Mar 21, 2024
e394f3b
squash migrations
sainak Mar 21, 2024
90779c6
fix key cache issue
sainak Mar 25, 2024
1070ee9
fix assetbed filter for middlewares
sainak Mar 25, 2024
faf6ad0
return asset presets from patient_from_asset route itself
sainak Mar 25, 2024
53159c5
add is_parsed_by_ocr flag to indicate ocr use for automated round
sainak Mar 25, 2024
cd992b7
improve regex match for hostname query
sainak Mar 25, 2024
9d37cac
Merge remote-tracking branch 'origin/develop' into sainak/feat/middle…
sainak Mar 25, 2024
b4ea44c
squash migrations
sainak Mar 25, 2024
01dd94c
Discard changes to config/settings/local.py
sainak Mar 25, 2024
76d2754
Merge remote-tracking branch 'origin/develop' into sainak/feat/middle…
sainak Apr 5, 2024
989c770
update migrations
sainak Apr 5, 2024
41f5920
Merge remote-tracking branch 'origin/develop' into sainak/feat/middle…
sainak Apr 8, 2024
1457312
add permission classes to test views
sainak Apr 8, 2024
ac92d0f
take jwks as kwarg for jwt generation
sainak Apr 8, 2024
567f2de
split open id auth response for easier mocking
sainak Apr 8, 2024
81597d9
test middleware auth
sainak Apr 8, 2024
87364d6
remove indexes
sainak Apr 9, 2024
b873d18
remove unnessary comments
sainak Apr 9, 2024
7f5270e
use request mocks to test openid auth
sainak Apr 10, 2024
5aa499d
make push config logic async
sainak Apr 10, 2024
7855ae3
improve query
sainak Apr 10, 2024
e682454
fix inconsistent validation
sainak Apr 10, 2024
47b41d4
Merge remote-tracking branch 'origin/develop' into sainak/feat/middle…
sainak Apr 10, 2024
0df52b4
fix signal
sainak Apr 10, 2024
a14e86c
Merge Develop
vigneshhari Apr 14, 2024
1d9f718
Relock dependencies
vigneshhari Apr 14, 2024
2044e57
update migrations
sainak Apr 14, 2024
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
99 changes: 95 additions & 4 deletions care/facility/api/serializers/asset.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import logging
from datetime import datetime

from django.core.cache import cache
from django.db import transaction
from django.db import models, transaction
from django.db.models import F, Value
from django.db.models.functions import Cast, Coalesce, NullIf
from django.shortcuts import get_object_or_404
from django.utils.timezone import now
from drf_spectacular.utils import extend_schema_field
Expand Down Expand Up @@ -33,11 +36,14 @@
from care.users.api.serializers.user import UserBaseMinimumSerializer
from care.utils.assetintegration.hl7monitor import HL7MonitorAsset
from care.utils.assetintegration.onvif import OnvifAsset
from care.utils.assetintegration.push_config import push_config_to_middleware
from care.utils.assetintegration.ventilator import VentilatorAsset
from care.utils.queryset.facility import get_facility_queryset
from config.serializers import ChoiceField
from config.validators import MiddlewareDomainAddressValidator

logger = logging.getLogger(__name__)


class AssetLocationSerializer(ModelSerializer):
facility = FacilityBareMinimumSerializer(read_only=True)
Expand Down Expand Up @@ -210,13 +216,55 @@
):
raise ValidationError({"asset_class": "Cannot change asset class"})

if meta := attrs.get("meta"):
current_location = attrs.get(

Check warning on line 220 in care/facility/api/serializers/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/asset.py#L220

Added line #L220 was not covered by tests
"current_location", self.instance.current_location
)
ip_address = meta.get("local_ip_address")
middleware_hostname = (

Check warning on line 224 in care/facility/api/serializers/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/asset.py#L223-L224

Added lines #L223 - L224 were not covered by tests
meta.get("middleware_hostname")
or current_location.middleware_address
or current_location.facility.middleware_address
)
if ip_address and middleware_hostname:
assets_using_ip = (

Check warning on line 230 in care/facility/api/serializers/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/asset.py#L230

Added line #L230 was not covered by tests
Asset.objects.filter(
current_location__facility=current_location.facility
)
.annotate(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting this to or was breaking the validation

resolved_middleware_hostname=Coalesce(
NullIf(
Cast(
F("meta__middleware_hostname"), models.CharField()
),
Value('""'),
),
NullIf(
F("current_location__middleware_address"), Value("")
),
F("current_location__facility__middleware_address"),
output_field=models.CharField(),
)
)
.filter(
resolved_middleware_hostname=middleware_hostname,
meta__local_ip_address=ip_address,
)
.exclude(id=self.instance.id if self.instance else None)
.values_list("name", flat=True)
)
if assets_using_ip:
raise ValidationError(

Check warning on line 257 in care/facility/api/serializers/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/asset.py#L257

Added line #L257 was not covered by tests
f"IP Address {ip_address} is already in use by {', '.join(assets_using_ip)} asset(s)"
)

return super().validate(attrs)

def create(self, validated_data):
last_serviced_on = validated_data.pop("last_serviced_on", None)
note = validated_data.pop("note", None)
with transaction.atomic():
asset_instance = super().create(validated_data)
asset_instance: Asset = super().create(validated_data)
if last_serviced_on or note:
asset_service = AssetService(
asset=asset_instance, serviced_on=last_serviced_on, note=note
Expand All @@ -226,7 +274,7 @@
asset_instance.save(update_fields=["last_service"])
return asset_instance

def update(self, instance, validated_data):
def update(self, instance: Asset, validated_data):
user = self.context["request"].user
with transaction.atomic():
if validated_data.get("last_serviced_on") and (
Expand Down Expand Up @@ -271,11 +319,54 @@
asset=instance,
performed_by=user,
).save()
updated_instance = super().update(instance, validated_data)
old_hostname = instance.resolved_middleware.get("hostname")
updated_instance: Asset = super().update(instance, validated_data)
cache.delete(f"asset:{instance.external_id}")
new_hostname = updated_instance.resolved_middleware.get("hostname")
response = push_config_to_middleware(
sainak marked this conversation as resolved.
Show resolved Hide resolved
new_hostname,
updated_instance.external_id,
AssetConfigSerializer(updated_instance).data,
old_hostname,
)
logger.info(f"Asset update response from middleware: {response}")
return updated_instance


class AssetConfigSerializer(ModelSerializer):
id = UUIDField(source="external_id")
type = CharField(source="asset_class")
description = CharField(default="")
ip_address = CharField(default="")
access_key = CharField(default="")
username = CharField(default="")
password = CharField(default="")
port = serializers.IntegerField(default=80)

def to_representation(self, instance: Asset):
data = super().to_representation(instance)
data["ip_address"] = instance.meta.get("local_ip_address")
if camera_access_key := instance.meta.get("camera_access_key"):
values = camera_access_key.split(":")

Check warning on line 350 in care/facility/api/serializers/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/asset.py#L350

Added line #L350 was not covered by tests
if len(values) == 3:
data["username"], data["password"], data["access_key"] = values

Check warning on line 352 in care/facility/api/serializers/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/serializers/asset.py#L352

Added line #L352 was not covered by tests
return data

class Meta:
model = Asset
fields = (
"id",
"name",
"type",
"description",
"ip_address",
"access_key",
"username",
"password",
"port",
)


class AssetTransactionSerializer(ModelSerializer):
id = UUIDField(source="external_id", read_only=True)
asset = AssetBareMinimumSerializer(read_only=True)
Expand Down
17 changes: 10 additions & 7 deletions care/facility/api/serializers/patient_consultation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
from care.abdm.utils.api_call import AbdmGateway
from care.facility.api.serializers import TIMESTAMP_FIELDS
from care.facility.api.serializers.asset import AssetLocationSerializer
from care.facility.api.serializers.bed import ConsultationBedSerializer
from care.facility.api.serializers.bed import (
AssetBedSerializer,
ConsultationBedSerializer,
)
from care.facility.api.serializers.consultation_diagnosis import (
ConsultationCreateDiagnosisSerializer,
ConsultationDiagnosisSerializer,
Expand Down Expand Up @@ -765,14 +768,14 @@ def create(self, validated_data):
raise NotImplementedError


class PatientConsultationIDSerializer(serializers.ModelSerializer):
consultation_id = serializers.UUIDField(source="external_id", read_only=True)
patient_id = serializers.UUIDField(source="patient.external_id", read_only=True)
bed_id = serializers.UUIDField(source="current_bed.bed.external_id", read_only=True)
class PatientConsultationIDSerializer(serializers.Serializer):
consultation_id = serializers.UUIDField(read_only=True)
patient_id = serializers.UUIDField(read_only=True)
bed_id = serializers.UUIDField(read_only=True)
asset_beds = AssetBedSerializer(many=True, read_only=True)

class Meta:
model = PatientConsultation
fields = ("consultation_id", "patient_id", "bed_id")
fields = ("consultation_id", "patient_id", "bed_id", "asset_beds")


class EmailDischargeSummarySerializer(serializers.Serializer):
Expand Down
81 changes: 79 additions & 2 deletions care/facility/api/viewsets/asset.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import re

from django.conf import settings
from django.core.cache import cache
from django.db.models import Exists, OuterRef, Q, Subquery
from django.db.models import CharField, Exists, F, OuterRef, Q, Subquery, Value
from django.db.models.functions import Cast, Coalesce, NullIf
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.http import Http404
Expand All @@ -9,7 +12,7 @@
from django_filters import rest_framework as filters
from django_filters.constants import EMPTY_VALUES
from djqscsv import render_to_csv_response
from drf_spectacular.utils import extend_schema, inline_serializer
from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer
from dry_rest_permissions.generics import DRYPermissions
from rest_framework import exceptions
from rest_framework import filters as drf_filters
Expand All @@ -29,6 +32,7 @@
from rest_framework.viewsets import GenericViewSet

from care.facility.api.serializers.asset import (
AssetConfigSerializer,
AssetLocationSerializer,
AssetSerializer,
AssetServiceSerializer,
Expand All @@ -54,10 +58,12 @@
from care.users.models import User
from care.utils.assetintegration.asset_classes import AssetClasses
from care.utils.assetintegration.base import BaseAssetIntegration
from care.utils.assetintegration.push_config import delete_asset_from_middleware
from care.utils.cache.cache_allowed_facilities import get_accessible_facilities
from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices
from care.utils.queryset.asset_location import get_asset_location_queryset
from care.utils.queryset.facility import get_facility_queryset
from config.authentication import MiddlewareAuthentication

inverse_asset_type = inverse_choices(AssetTypeChoices)
inverse_asset_status = inverse_choices(StatusChoices)
Expand Down Expand Up @@ -305,6 +311,11 @@
)
return queryset

def perform_destroy(self, instance: Asset) -> None:
if hostname := instance.resolved_middleware:
delete_asset_from_middleware(hostname["hostname"], instance.external_id)
return super().perform_destroy(instance)

def list(self, request, *args, **kwargs):
if settings.CSV_REQUEST_PARAMETER in request.GET:
mapping = Asset.CSV_MAPPING.copy()
Expand Down Expand Up @@ -413,6 +424,72 @@
)


class AssetRetrieveConfigViewSet(ListModelMixin, GenericViewSet):
queryset = Asset.objects.all()
authentication_classes = [MiddlewareAuthentication]
permission_classes = [IsAuthenticated]
serializer_class = AssetConfigSerializer

@extend_schema(
tags=["asset"],
parameters=[
OpenApiParameter(
name="middleware_hostname",
location=OpenApiParameter.QUERY,
)
],
)
def list(self, request, *args, **kwargs):
"""
This API is used by the middleware to retrieve assets and their configurations
for a given facility and middleware hostname.
"""
middleware_hostname = request.query_params.get("middleware_hostname")

Check warning on line 447 in care/facility/api/viewsets/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/asset.py#L447

Added line #L447 was not covered by tests
if not middleware_hostname:
return Response(

Check warning on line 449 in care/facility/api/viewsets/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/asset.py#L449

Added line #L449 was not covered by tests
{"middleware_hostname": "Middleware hostname is required"},
status=status.HTTP_400_BAD_REQUEST,
)

if match := re.match(r"^(https?://)?([^\s/]+)/?$", middleware_hostname):
middleware_hostname = match.group(2) # extract the hostname from the URL

Check warning on line 455 in care/facility/api/viewsets/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/asset.py#L455

Added line #L455 was not covered by tests
else:
return Response(

Check warning on line 457 in care/facility/api/viewsets/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/asset.py#L457

Added line #L457 was not covered by tests
{"middleware_hostname": "Invalid middleware hostname"},
status=status.HTTP_400_BAD_REQUEST,
)

queryset = (

Check warning on line 462 in care/facility/api/viewsets/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/asset.py#L462

Added line #L462 was not covered by tests
self.get_queryset()
.filter(
current_location__facility=self.request.user.facility,
asset_class__in=[
AssetClasses.ONVIF.name,
AssetClasses.HL7MONITOR.name,
],
)
.annotate(
resolved_middleware_hostname=Coalesce(
NullIf(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See if you can use the or statement here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting this to or was returning assets from other hostnames as well

Cast(F("meta__middleware_hostname"), CharField()),
Value('""'),
),
NullIf(F("current_location__middleware_address"), Value("")),
F("current_location__facility__middleware_address"),
output_field=CharField(),
)
)
.filter(resolved_middleware_hostname=middleware_hostname)
.exclude(
Q(meta__local_ip_address__isnull=True)
| Q(meta__local_ip_address__exact=""),
)
).only("external_id", "meta", "description", "name", "asset_class")

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

Check warning on line 490 in care/facility/api/viewsets/asset.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/asset.py#L489-L490

Added lines #L489 - L490 were not covered by tests


class AssetTransactionFilter(filters.FilterSet):
qr_code_id = filters.CharFilter(field_name="asset__qr_code_id")
external_id = filters.CharFilter(field_name="asset__external_id")
Expand Down
4 changes: 2 additions & 2 deletions care/facility/api/viewsets/mixins/access.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from care.facility.models.mixins.permissions.asset import DRYAssetPermissions
from care.users.models import User
from config.authentication import MiddlewareAuthentication
from config.authentication import MiddlewareAssetAuthentication


class UserAccessMixin:
Expand Down Expand Up @@ -55,7 +55,7 @@ class AssetUserAccessMixin:
asset_permissions = (DRYAssetPermissions,)

def get_authenticators(self):
return [MiddlewareAuthentication()] + super().get_authenticators()
return [MiddlewareAssetAuthentication()] + super().get_authenticators()

def get_permissions(self):
"""
Expand Down
5 changes: 4 additions & 1 deletion care/facility/api/viewsets/open_id.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import AllowAny
from rest_framework.response import Response


class OpenIdConfigView(GenericAPIView):
class PublicJWKsView(GenericAPIView):
"""
Retrieve the OpenID Connect configuration
"""

authentication_classes = ()
permission_classes = (AllowAny,)

@method_decorator(cache_page(60 * 60 * 24))
def get(self, *args, **kwargs):
return Response(settings.JWKS.as_dict())
4 changes: 2 additions & 2 deletions care/facility/api/viewsets/patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
from config.authentication import (
CustomBasicAuthentication,
CustomJWTAuthentication,
MiddlewareAuthentication,
MiddlewareAssetAuthentication,
)

REVERSE_FACILITY_TYPES = covert_choice_dict(FACILITY_TYPES)
Expand Down Expand Up @@ -329,7 +329,7 @@ class PatientViewSet(
authentication_classes = [
CustomBasicAuthentication,
CustomJWTAuthentication,
MiddlewareAuthentication,
MiddlewareAssetAuthentication,
]
permission_classes = (IsAuthenticated, DRYPermissions)
lookup_field = "external_id"
Expand Down
Loading
Loading