UPDATE
This commit is contained in:
@@ -25,6 +25,7 @@ ACCESS_CONTROL_AUTHZ_ENABLED=true
|
|||||||
ACCESS_CONTROL_AUTHZ_BASE_URL=http://croplogic-accsess-opa:8181
|
ACCESS_CONTROL_AUTHZ_BASE_URL=http://croplogic-accsess-opa:8181
|
||||||
ACCESS_CONTROL_AUTHZ_BATCH_PATH=/v1/data/croplogic/authz/batch_decision
|
ACCESS_CONTROL_AUTHZ_BATCH_PATH=/v1/data/croplogic/authz/batch_decision
|
||||||
ACCESS_CONTROL_AUTHZ_TIMEOUT=30
|
ACCESS_CONTROL_AUTHZ_TIMEOUT=30
|
||||||
|
ACCESS_CONTROL_AUTHZ_CACHE_TIMEOUT=300
|
||||||
|
|
||||||
AI_SERVICE_BASE_URL=https://ai.example.com
|
AI_SERVICE_BASE_URL=https://ai.example.com
|
||||||
AI_SERVICE_API_KEY=
|
AI_SERVICE_API_KEY=
|
||||||
@@ -43,6 +44,7 @@ CROP_ZONE_TASK_STALE_SECONDS=300
|
|||||||
|
|
||||||
CELERY_BROKER_URL=redis://redis:6379/0
|
CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||||
|
CACHE_URL=redis://redis:6379/0
|
||||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP=true
|
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP=true
|
||||||
QDRANT_HOST=qdrant
|
QDRANT_HOST=qdrant
|
||||||
QDRANT_PORT=6333
|
QDRANT_PORT=6333
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
from django.http import JsonResponse
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
||||||
|
|
||||||
|
from farm_hub.models import FarmHub
|
||||||
|
|
||||||
|
from .services import (
|
||||||
|
AccessControlServiceUnavailable,
|
||||||
|
authorize_feature,
|
||||||
|
get_authorization_action,
|
||||||
|
get_request_data,
|
||||||
|
get_route_feature_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RouteFeatureAccessMiddleware(MiddlewareMixin):
|
||||||
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||||
|
view_class = getattr(view_func, "view_class", None)
|
||||||
|
if view_class is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._allows_anonymous(view_class):
|
||||||
|
return None
|
||||||
|
|
||||||
|
user = self._get_authenticated_user(request)
|
||||||
|
if user is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
app_label = view_class.__module__.split(".", 1)[0]
|
||||||
|
feature_code = get_route_feature_code(app_label)
|
||||||
|
if not feature_code:
|
||||||
|
return None
|
||||||
|
|
||||||
|
farm_uuid = view_kwargs.get("farm_uuid") or request.GET.get("farm_uuid") or get_request_data(request).get("farm_uuid")
|
||||||
|
farm = None
|
||||||
|
if farm_uuid:
|
||||||
|
try:
|
||||||
|
farm = FarmHub.objects.select_related("farm_type", "subscription_plan").prefetch_related(
|
||||||
|
"products",
|
||||||
|
"sensors",
|
||||||
|
"sensors__sensor_catalog",
|
||||||
|
).get(farm_uuid=farm_uuid, owner=user)
|
||||||
|
except FarmHub.DoesNotExist:
|
||||||
|
return JsonResponse(
|
||||||
|
{"code": 403, "msg": f"Access to route feature `{feature_code}` is denied."},
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
allowed = authorize_feature(
|
||||||
|
farm=farm,
|
||||||
|
user=user,
|
||||||
|
feature_code=feature_code,
|
||||||
|
action=get_authorization_action(request.method),
|
||||||
|
route=request.path,
|
||||||
|
)
|
||||||
|
except AccessControlServiceUnavailable as exc:
|
||||||
|
return JsonResponse({"code": 503, "msg": str(exc)}, status=503)
|
||||||
|
|
||||||
|
if not allowed:
|
||||||
|
return JsonResponse(
|
||||||
|
{"code": 403, "msg": f"Access to route feature `{feature_code}` is denied."},
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
|
request.route_feature_code = feature_code
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _allows_anonymous(view_class):
|
||||||
|
for permission_class in getattr(view_class, "permission_classes", []):
|
||||||
|
if permission_class is AllowAny:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_authenticated_user(request):
|
||||||
|
if getattr(request, "user", None) is not None and request.user.is_authenticated:
|
||||||
|
return request.user
|
||||||
|
|
||||||
|
authenticator = JWTAuthentication()
|
||||||
|
try:
|
||||||
|
auth_result = authenticator.authenticate(request)
|
||||||
|
except (InvalidToken, TokenError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if auth_result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
user, _token = auth_result
|
||||||
|
request.user = user
|
||||||
|
request._cached_user = user
|
||||||
|
return user
|
||||||
@@ -33,7 +33,13 @@ class FeatureAccessPermission(BasePermission):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
allowed = authorize_feature(farm, request.user, feature_code, get_authorization_action(request.method))
|
allowed = authorize_feature(
|
||||||
|
farm,
|
||||||
|
request.user,
|
||||||
|
feature_code,
|
||||||
|
get_authorization_action(request.method),
|
||||||
|
route=request.path,
|
||||||
|
)
|
||||||
except AccessControlServiceUnavailable as exc:
|
except AccessControlServiceUnavailable as exc:
|
||||||
self.message = str(exc)
|
self.message = str(exc)
|
||||||
return False
|
return False
|
||||||
|
|||||||
+114
-12
@@ -1,7 +1,12 @@
|
|||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from farm_hub.models import FarmHub
|
from farm_hub.models import FarmHub
|
||||||
@@ -29,6 +34,38 @@ ACTION_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_authz_cache_timeout():
|
||||||
|
return int(getattr(settings, "ACCESS_CONTROL_AUTHZ_CACHE_TIMEOUT", 300))
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def load_route_feature_map():
|
||||||
|
feature_map_path = Path(settings.BASE_DIR) / "config" / "feature.json"
|
||||||
|
with feature_map_path.open("r", encoding="utf-8") as feature_map_file:
|
||||||
|
return json.load(feature_map_file)
|
||||||
|
|
||||||
|
|
||||||
|
def get_route_feature_code(app_label):
|
||||||
|
if not app_label:
|
||||||
|
return None
|
||||||
|
return load_route_feature_map().get(app_label)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_authorization_cache_key(farm, user, features, action, route):
|
||||||
|
raw_key = json.dumps(
|
||||||
|
{
|
||||||
|
"farm_uuid": str(getattr(farm, "farm_uuid", "")),
|
||||||
|
"user_id": getattr(user, "id", None),
|
||||||
|
"features": sorted(features),
|
||||||
|
"action": action,
|
||||||
|
"route": route or "",
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
digest = hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
|
||||||
|
return f"access-control:authz:{digest}"
|
||||||
|
|
||||||
|
|
||||||
def get_default_subscription_plan():
|
def get_default_subscription_plan():
|
||||||
return SubscriptionPlan.objects.filter(is_active=True, metadata__is_default=True).order_by("name").first()
|
return SubscriptionPlan.objects.filter(is_active=True, metadata__is_default=True).order_by("name").first()
|
||||||
|
|
||||||
@@ -153,6 +190,18 @@ def build_farm_access_profile(farm):
|
|||||||
|
|
||||||
|
|
||||||
def build_opa_resource(farm):
|
def build_opa_resource(farm):
|
||||||
|
if farm is None:
|
||||||
|
return {
|
||||||
|
"farm_id": None,
|
||||||
|
"subscription_plan_codes": [],
|
||||||
|
"farm_types": [],
|
||||||
|
"crop_types": [],
|
||||||
|
"cultivation_types": [],
|
||||||
|
"sensor_codes": [],
|
||||||
|
"power_sensor": [],
|
||||||
|
"customization": [],
|
||||||
|
}
|
||||||
|
|
||||||
subscription_plan = get_effective_subscription_plan(farm)
|
subscription_plan = get_effective_subscription_plan(farm)
|
||||||
sensor_codes = list(
|
sensor_codes = list(
|
||||||
farm.sensors.exclude(sensor_catalog__isnull=True).values_list("sensor_catalog__code", flat=True)
|
farm.sensors.exclude(sensor_catalog__isnull=True).values_list("sensor_catalog__code", flat=True)
|
||||||
@@ -199,23 +248,24 @@ def _opa_url(path):
|
|||||||
return urljoin(f"{base_url.rstrip('/')}/", path.lstrip("/"))
|
return urljoin(f"{base_url.rstrip('/')}/", path.lstrip("/"))
|
||||||
|
|
||||||
|
|
||||||
def build_authorization_input(farm, user, features, action):
|
def build_authorization_input(farm, user, features, action, route=None):
|
||||||
return {
|
return {
|
||||||
"user": build_opa_user(user),
|
"user": build_opa_user(user),
|
||||||
"resource": build_opa_resource(farm),
|
"resource": build_opa_resource(farm),
|
||||||
"features": list(features),
|
"features": list(features),
|
||||||
"action": action,
|
"action": action,
|
||||||
|
"route": route,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def request_opa_batch_authorization(farm, user, features, action):
|
def request_opa_batch_authorization(farm, user, features, action, route=None):
|
||||||
if not getattr(settings, "ACCESS_CONTROL_AUTHZ_ENABLED", True):
|
if not getattr(settings, "ACCESS_CONTROL_AUTHZ_ENABLED", True):
|
||||||
return {"decisions": {feature: True for feature in features}}
|
return {"decisions": {feature: True for feature in features}}
|
||||||
|
|
||||||
if not features:
|
if not features:
|
||||||
return {"decisions": {}}
|
return {"decisions": {}}
|
||||||
|
|
||||||
payload = {"input": build_authorization_input(farm, user, features, action)}
|
payload = {"input": build_authorization_input(farm, user, features, action, route=route)}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
@@ -238,6 +288,17 @@ def normalize_opa_batch_result(data, features):
|
|||||||
if isinstance(decisions, dict):
|
if isinstance(decisions, dict):
|
||||||
return {feature: bool(decisions.get(feature, False)) for feature in features}
|
return {feature: bool(decisions.get(feature, False)) for feature in features}
|
||||||
|
|
||||||
|
feature_results = data.get("features")
|
||||||
|
if isinstance(feature_results, dict):
|
||||||
|
normalized = {}
|
||||||
|
for feature in features:
|
||||||
|
feature_result = feature_results.get(feature, {})
|
||||||
|
if isinstance(feature_result, dict):
|
||||||
|
normalized[feature] = bool(feature_result.get("allow", False))
|
||||||
|
else:
|
||||||
|
normalized[feature] = bool(feature_result)
|
||||||
|
return normalized
|
||||||
|
|
||||||
allowed_features = data.get("allowed_features")
|
allowed_features = data.get("allowed_features")
|
||||||
if isinstance(allowed_features, list):
|
if isinstance(allowed_features, list):
|
||||||
allowed = set(allowed_features)
|
allowed = set(allowed_features)
|
||||||
@@ -249,18 +310,59 @@ def normalize_opa_batch_result(data, features):
|
|||||||
raise AccessControlServiceUnavailable("OPA authorization service returned an unsupported payload.")
|
raise AccessControlServiceUnavailable("OPA authorization service returned an unsupported payload.")
|
||||||
|
|
||||||
|
|
||||||
def batch_authorize_features(farm, user, features, action):
|
def batch_authorize_features(farm, user, features, action, route=None):
|
||||||
result = request_opa_batch_authorization(farm, user, features, action)
|
if not features:
|
||||||
return normalize_opa_batch_result(result, features)
|
return {}
|
||||||
|
|
||||||
|
cache_key = _get_authorization_cache_key(farm, user, features, action, route)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cached_result = cache.get(cache_key)
|
||||||
|
except Exception:
|
||||||
|
cached_result = None
|
||||||
|
|
||||||
|
if isinstance(cached_result, dict):
|
||||||
|
return {feature: bool(cached_result.get(feature, False)) for feature in features}
|
||||||
|
|
||||||
|
result = request_opa_batch_authorization(farm, user, features, action, route=route)
|
||||||
|
decisions = normalize_opa_batch_result(result, features)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache.set(cache_key, decisions, timeout=_get_authz_cache_timeout())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return decisions
|
||||||
|
|
||||||
|
|
||||||
def authorize_feature(farm, user, feature_code, action):
|
def authorize_feature(farm, user, feature_code, action, route=None):
|
||||||
return batch_authorize_features(farm, user, [feature_code], action).get(feature_code, False)
|
return batch_authorize_features(farm, user, [feature_code], action, route=route).get(feature_code, False)
|
||||||
|
|
||||||
|
|
||||||
def get_request_data(request):
|
def get_request_data(request):
|
||||||
if isinstance(request.data, QueryDict):
|
request_data = getattr(request, "data", None)
|
||||||
return request.data
|
if isinstance(request_data, QueryDict):
|
||||||
if isinstance(request.data, dict):
|
return request_data
|
||||||
return request.data
|
if isinstance(request_data, dict):
|
||||||
|
return request_data
|
||||||
|
|
||||||
|
cached_body = getattr(request, "_access_control_request_data", None)
|
||||||
|
if isinstance(cached_body, dict):
|
||||||
|
return cached_body
|
||||||
|
|
||||||
|
content_type = (getattr(request, "content_type", "") or "").split(";")[0].strip().lower()
|
||||||
|
body = getattr(request, "body", b"") or b""
|
||||||
|
if not body:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if content_type == "application/json":
|
||||||
|
try:
|
||||||
|
parsed_body = json.loads(body.decode("utf-8"))
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
if isinstance(parsed_body, dict):
|
||||||
|
request._access_control_request_data = parsed_body
|
||||||
|
return parsed_body
|
||||||
|
return {}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import RequestFactory, SimpleTestCase, override_settings
|
||||||
|
|
||||||
|
from account.views import ProfileView
|
||||||
|
|
||||||
|
from .middleware import RouteFeatureAccessMiddleware
|
||||||
|
from .services import batch_authorize_features, build_authorization_input
|
||||||
|
|
||||||
|
|
||||||
|
TEST_CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||||
|
"LOCATION": "access-control-tests",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(CACHES=TEST_CACHES, ACCESS_CONTROL_AUTHZ_CACHE_TIMEOUT=300)
|
||||||
|
class AccessControlServiceTests(SimpleTestCase):
|
||||||
|
def test_batch_authorize_features_uses_cache_for_same_route(self):
|
||||||
|
farm = SimpleNamespace(farm_uuid="farm-uuid")
|
||||||
|
user = SimpleNamespace(id=7)
|
||||||
|
|
||||||
|
with patch("access_control.services.request_opa_batch_authorization") as mock_request:
|
||||||
|
mock_request.return_value = {"decisions": {"farm_dashboard": True}}
|
||||||
|
|
||||||
|
first_result = batch_authorize_features(
|
||||||
|
farm=farm,
|
||||||
|
user=user,
|
||||||
|
features=["farm_dashboard"],
|
||||||
|
action="view",
|
||||||
|
route="/api/farm-dashboard/",
|
||||||
|
)
|
||||||
|
second_result = batch_authorize_features(
|
||||||
|
farm=farm,
|
||||||
|
user=user,
|
||||||
|
features=["farm_dashboard"],
|
||||||
|
action="view",
|
||||||
|
route="/api/farm-dashboard/",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(first_result, {"farm_dashboard": True})
|
||||||
|
self.assertEqual(second_result, {"farm_dashboard": True})
|
||||||
|
self.assertEqual(mock_request.call_count, 1)
|
||||||
|
|
||||||
|
def test_build_authorization_input_includes_route(self):
|
||||||
|
user = SimpleNamespace(
|
||||||
|
id=3,
|
||||||
|
username="tester",
|
||||||
|
email="tester@example.com",
|
||||||
|
phone_number="09120000000",
|
||||||
|
is_staff=False,
|
||||||
|
is_superuser=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = build_authorization_input(
|
||||||
|
farm=None,
|
||||||
|
user=user,
|
||||||
|
features=["account_management"],
|
||||||
|
action="view",
|
||||||
|
route="/api/account/profile/",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(payload["route"], "/api/account/profile/")
|
||||||
|
self.assertEqual(payload["resource"]["sensor_codes"], [])
|
||||||
|
|
||||||
|
def test_batch_authorize_features_supports_nested_opa_feature_payload(self):
|
||||||
|
farm = SimpleNamespace(farm_uuid="farm-uuid")
|
||||||
|
user = SimpleNamespace(id=9)
|
||||||
|
|
||||||
|
with patch("access_control.services.request_opa_batch_authorization") as mock_request:
|
||||||
|
mock_request.return_value = {
|
||||||
|
"features": {
|
||||||
|
"feature1": {"allow": True, "allow_rules": [], "deny_rules": []},
|
||||||
|
"feature2": {"allow": False, "allow_rules": [], "deny_rules": []},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = batch_authorize_features(
|
||||||
|
farm=farm,
|
||||||
|
user=user,
|
||||||
|
features=["feature1", "feature2", "feature3"],
|
||||||
|
action="view",
|
||||||
|
route="/api/farm-dashboard/",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
{
|
||||||
|
"feature1": True,
|
||||||
|
"feature2": False,
|
||||||
|
"feature3": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RouteFeatureAccessMiddlewareTests(SimpleTestCase):
|
||||||
|
def test_middleware_passes_route_feature_and_method_to_service(self):
|
||||||
|
factory = RequestFactory()
|
||||||
|
request = factory.patch("/api/account/profile/")
|
||||||
|
request.user = SimpleNamespace(is_authenticated=True, id=11)
|
||||||
|
|
||||||
|
middleware = RouteFeatureAccessMiddleware(lambda req: None)
|
||||||
|
view = ProfileView.as_view()
|
||||||
|
|
||||||
|
with patch("access_control.middleware.authorize_feature", return_value=True) as mock_authorize:
|
||||||
|
response = middleware.process_view(request, view, (), {})
|
||||||
|
|
||||||
|
self.assertIsNone(response)
|
||||||
|
mock_authorize.assert_called_once_with(
|
||||||
|
farm=None,
|
||||||
|
user=request.user,
|
||||||
|
feature_code="account_management",
|
||||||
|
action="edit",
|
||||||
|
route="/api/account/profile/",
|
||||||
|
)
|
||||||
@@ -41,6 +41,7 @@ class FarmFeatureAuthorizationView(APIView):
|
|||||||
user=request.user,
|
user=request.user,
|
||||||
features=serializer.validated_data["features"],
|
features=serializer.validated_data["features"],
|
||||||
action=serializer.validated_data["action"],
|
action=serializer.validated_data["action"],
|
||||||
|
route=request.path,
|
||||||
)
|
)
|
||||||
except AccessControlServiceUnavailable as exc:
|
except AccessControlServiceUnavailable as exc:
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"auth": "auth_access",
|
||||||
|
"account": "account_management",
|
||||||
|
"farm_hub": "farm_management",
|
||||||
|
"access_control": "access_control",
|
||||||
|
"sensor_catalog": "sensor_catalog",
|
||||||
|
"dashboard": "farm_dashboard",
|
||||||
|
"crop_zoning": "crop_zoning",
|
||||||
|
"plant_simulator": "plant_simulator",
|
||||||
|
"pest_detection": "pest_detection",
|
||||||
|
"irrigation_recommendation": "irrigation_recommendation",
|
||||||
|
"fertilization_recommendation": "fertilization_recommendation",
|
||||||
|
"farm_ai_assistant": "farm_ai_assistant",
|
||||||
|
"notifications": "notifications",
|
||||||
|
"external_api_adapter": "external_api_adapter",
|
||||||
|
"sensor_external_api": "sensor_external_api"
|
||||||
|
}
|
||||||
+5
-2
@@ -53,6 +53,7 @@ MIDDLEWARE = [
|
|||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"access_control.middleware.RouteFeatureAccessMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
@@ -109,8 +110,9 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
||||||
"LOCATION": "croplogic-auth-otp",
|
"LOCATION": os.getenv("CACHE_URL", os.getenv("CELERY_BROKER_URL", "redis://redis:6379/0")),
|
||||||
|
"KEY_PREFIX": "croplogic",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +168,7 @@ ACCESS_CONTROL_AUTHZ_BASE_URL = os.getenv(
|
|||||||
)
|
)
|
||||||
ACCESS_CONTROL_AUTHZ_BATCH_PATH = os.getenv("ACCESS_CONTROL_AUTHZ_BATCH_PATH", "/v1/data/croplogic/authz/batch_decision")
|
ACCESS_CONTROL_AUTHZ_BATCH_PATH = os.getenv("ACCESS_CONTROL_AUTHZ_BATCH_PATH", "/v1/data/croplogic/authz/batch_decision")
|
||||||
ACCESS_CONTROL_AUTHZ_TIMEOUT = int(os.getenv("ACCESS_CONTROL_AUTHZ_TIMEOUT", str(EXTERNAL_API_TIMEOUT)))
|
ACCESS_CONTROL_AUTHZ_TIMEOUT = int(os.getenv("ACCESS_CONTROL_AUTHZ_TIMEOUT", str(EXTERNAL_API_TIMEOUT)))
|
||||||
|
ACCESS_CONTROL_AUTHZ_CACHE_TIMEOUT = int(os.getenv("ACCESS_CONTROL_AUTHZ_CACHE_TIMEOUT", "300"))
|
||||||
|
|
||||||
EXTERNAL_SERVICES = {
|
EXTERNAL_SERVICES = {
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
Reference in New Issue
Block a user