From 8579f9ae917f2c5eda54d423b2a1fd6c4fbf71b5 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Thu, 9 Apr 2026 23:25:59 +0330 Subject: [PATCH] UPDATE --- README.md | 49 +++++++++++++++++++++++ config/opa-config.yaml | 4 ++ docker-compose.yaml | 21 ++++++++++ policies/authz.rego | 68 ++++++++++++++++++++++++++++++++ policies/bootstrap-policies.json | 3 ++ policies/sensor_7_in_1.rego | 43 ++++++++++++++++++++ 6 files changed, 188 insertions(+) create mode 100644 README.md create mode 100644 config/opa-config.yaml create mode 100644 docker-compose.yaml create mode 100644 policies/authz.rego create mode 100644 policies/bootstrap-policies.json create mode 100644 policies/sensor_7_in_1.rego diff --git a/README.md b/README.md new file mode 100644 index 0000000..c965984 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# CropLogic Authorization Service + +This service runs OPA as a standalone authorization engine for `backend/access_control`. + +## Run standalone + +```bash +docker compose -f accsess/docker-compose.yaml up -d +``` + +## Decision endpoints + +- Single feature: `POST /v1/data/croplogic/authz/decision` +- Batch features: `POST /v1/data/croplogic/authz/batch_decision` + +The backend uses the batch endpoint and sends the farm context only. Users are treated as `farmer` by default inside the service, and features are allowed unless there is a feature-specific rule in `policies/authz.rego`. + +## Example request + +```bash +curl -s http://127.0.0.1:8181/v1/data/croplogic/authz/batch_decision \ + -H 'Content-Type: application/json' \ + -d @- <<'EOF' +{ + "input": { + "resource": { + "farm_id": "farm-1001", + "subscription_plan_codes": ["gold"], + "farm_types": ["greenhouse"], + "crop_types": ["tomato"], + "cultivation_types": ["soil"], + "sensor_codes": ["sensor-7-in-1"], + "power_sensor": ["main-power"], + "customization": ["default-layout"] + }, + "features": ["sensor-7-in-1"], + "action": "view" + } +} +EOF +``` + +## Add new rules in code + +Define feature-specific checks directly in `policies/authz.rego`. + +- If a feature has no rule, every action is allowed. +- If a feature rule exists, its conditions are evaluated and any failing condition denies access. +- `sensor-7-in-1` currently requires `resource.sensor_codes` to include `sensor-7-in-1`. diff --git a/config/opa-config.yaml b/config/opa-config.yaml new file mode 100644 index 0000000..6ceb01f --- /dev/null +++ b/config/opa-config.yaml @@ -0,0 +1,4 @@ +services: {} +labels: + app: croplogic-authz +plugins: {} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..251a2ed --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,21 @@ +services: + opa: + image: mirror-docker.runflare.com/openpolicyagent/opa + container_name: croplogic-accsess-opa + command: + - run + - --server + - --addr=0.0.0.0:8181 + - /policies + ports: + - "8181:8181" + volumes: + - ./policies:/policies:ro + - ./config/opa-config.yaml:/config/opa-config.yaml:ro + restart: unless-stopped + + networks: + - crop_network +networks: + crop_network: + external: true \ No newline at end of file diff --git a/policies/authz.rego b/policies/authz.rego new file mode 100644 index 0000000..cad364e --- /dev/null +++ b/policies/authz.rego @@ -0,0 +1,68 @@ +package croplogic.authz + +import rego.v1 + +default allow := false + +allow if { + decision.allow +} + +decision := feature_decision(input.feature) + +batch_decision := { + "features": { + feature: result | + feature := input.features[_] + result := feature_decision(feature) + }, +} + +feature_decision(feature) := { + "allow": true, + "matched_rules": [], + "deny_rules": [], + "allow_rules": [], +} if { + not has_feature_rule(feature) +} + +feature_decision(feature) := result if { + has_feature_rule(feature) + rule := feature_rule(feature) + matched := [matched_rule | matched_rule := rule; action_match(matched_rule)] + deny_rules := [matched_rule | matched_rule := matched[_]; not object.get(matched_rule, "allow", false)] + allow_rules := [matched_rule | matched_rule := matched[_]; object.get(matched_rule, "allow", false)] + count(deny_rules) == 0 + result := { + "allow": true, + "matched_rules": matched, + "deny_rules": deny_rules, + "allow_rules": allow_rules, + } +} + +feature_decision(feature) := result if { + has_feature_rule(feature) + rule := feature_rule(feature) + matched := [matched_rule | matched_rule := rule; action_match(matched_rule)] + deny_rules := [matched_rule | matched_rule := matched[_]; not object.get(matched_rule, "allow", false)] + allow_rules := [matched_rule | matched_rule := matched[_]; object.get(matched_rule, "allow", false)] + count(deny_rules) > 0 + result := { + "allow": false, + "matched_rules": matched, + "deny_rules": deny_rules, + "allow_rules": allow_rules, + } +} + +action_match(rule) if { + count(object.get(rule, "actions_any", [])) == 0 +} + +action_match(rule) if { + requested_action := lower(sprintf("%v", [object.get(input, "action", "view")])) + action := object.get(rule, "actions_any", [])[_] + lower(sprintf("%v", [action])) == requested_action +} diff --git a/policies/bootstrap-policies.json b/policies/bootstrap-policies.json new file mode 100644 index 0000000..53fd235 --- /dev/null +++ b/policies/bootstrap-policies.json @@ -0,0 +1,3 @@ +{ + "authz": {} +} diff --git a/policies/sensor_7_in_1.rego b/policies/sensor_7_in_1.rego new file mode 100644 index 0000000..5fc97cc --- /dev/null +++ b/policies/sensor_7_in_1.rego @@ -0,0 +1,43 @@ +package croplogic.authz + +import rego.v1 + +has_feature_rule(feature) if { + is_sensor_7_in_1_feature(feature) +} + +feature_rule(feature) := { + "code": "sensor-7-in-1-requires-sensor-code", + "allow": true, + "reason": "sensor-7-in-1 feature requires sensor_codes to include sensor-7-in-1", +} if { + is_sensor_7_in_1_feature(feature) + has_sensor_code("sensor-7-in-1") +} + +feature_rule(feature) := { + "code": "sensor-7-in-1-requires-sensor-code", + "allow": false, + "reason": "sensor-7-in-1 feature requires sensor_codes to include sensor-7-in-1", +} if { + is_sensor_7_in_1_feature(feature) + not has_sensor_code("sensor-7-in-1") +} + +is_sensor_7_in_1_feature(feature) if { + lower(sprintf("%v", [feature])) == "sensor-7-in-1" +} + +has_sensor_code(code) if { + sensor_codes := object.get(input.resource, "sensor_codes", []) + is_array(sensor_codes) + sensor_code := sensor_codes[_] + lower(sprintf("%v", [sensor_code])) == lower(sprintf("%v", [code])) +} + +has_sensor_code(code) if { + sensor_code := object.get(input.resource, "sensor_codes", null) + sensor_code != null + not is_array(sensor_code) + lower(sprintf("%v", [sensor_code])) == lower(sprintf("%v", [code])) +}