This commit is contained in:
2026-04-09 23:25:59 +03:30
commit 8579f9ae91
6 changed files with 188 additions and 0 deletions
+49
View File
@@ -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`.
+4
View File
@@ -0,0 +1,4 @@
services: {}
labels:
app: croplogic-authz
plugins: {}
+21
View File
@@ -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
+68
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
{
"authz": {}
}
+43
View File
@@ -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]))
}