diff --git a/example/data/logs.rego b/example/data/logs.rego index 9752d25..c629d1a 100644 --- a/example/data/logs.rego +++ b/example/data/logs.rego @@ -1,9 +1,13 @@ package system.log # Mask the 'token' field in the query parameters -mask contains "/input/query/token" +mask contains {"op":"upsert","path":"/input/query/token", "value": "**redacted**"} if { + input.input.query.token +} # Mask the 'x-api-key' field in the headers parameters -mask contains "/input/headers/x-api-key" +mask contains {"op":"upsert","path":"/input/headers/x-api-key", "value": "**redacted**"} if { + input.input.headers["x-api-key"] +} drop if { input.result.allowed == true diff --git a/example/data/policy.rego b/example/data/policy.rego index 2557aff..6546c69 100644 --- a/example/data/policy.rego +++ b/example/data/policy.rego @@ -23,6 +23,8 @@ tokens := [token | token := temp ] +default claims = {"payload": {}} + claims := {"payload": payload, "valid": valid, "kid": kid} if { token := tokens[_] [header, payload, _] := io.jwt.decode(token) @@ -38,29 +40,29 @@ userData := data.users[claims.payload.sub] if { claims.payload.sub != "c2b" } -deny contains "no token supplied in any of the possible locations" if { +deny contains {"reason":"no token supplied in any of the possible locations", "code": "TOKEN_MISSING"} if { count(tokens) == 0 } -deny contains "token environment mismatch" if { - claims.kid != data.keys.kid -} +# deny contains {"reason":"token environment mismatch", "code": "TOKEN_MISMATCH"} if { +# claims.kid != data.keys.kid +# } -deny contains "token not valid" if { +deny contains {"reason":"token not valid", "code": "TOKEN_INVALID"} if { not claims.valid count(tokens) > 0 } -deny contains "the token is valid, but the user is not found" if { +deny contains {"reason":"the token is valid, but the user is not found", "code": "USER_NOT_FOUND"} if { claims.valid not userData } -deny contains "domain missing" if { +deny contains {"reason":"domain missing", "code": "DOMAIN_MISSING"} if { not input.domain } -deny contains "domain check failed" if { +deny contains {"reason":"domain not allowed for this user", "code": "DOMAIN_INCORRECT"} if { every domain in userData.domains { domain != input.domain } } @@ -70,7 +72,7 @@ is_origin_invalid(originHeader, allowedOrigin) if { } -deny contains "origin check failed" if { +deny contains {"reason":"origin check failed", "code": "ORIGIN_CHECK_FAILED"} if { originHeader := object.get(headers, "origin", "A") every origin in userData.origins { is_origin_invalid(originHeader, origin) } @@ -89,17 +91,17 @@ need_user_agent := true if { } -deny contains "user-agent missing" if { +deny contains {"reason":"user-agent missing", "code": "USER_AGENT_MISSING"} if { not headers["user-agent"] need_user_agent } -deny contains "user-agent is not from allowed browsers" if { +deny contains {"reason":"user-agent is not from allowed browsers", "code": "USER_AGENT_NOT_ALLOWED"} if { not userData.allowNoBrowser not regex.match(".*(Gecko|AppleWebKit|Opera|Trident|Edge|Chrome)\\/\\d.*$", headers["user-agent"]) } -deny contains "c2b user only allowed from QGIS or ARCGIS" if { +deny contains {"reason":"c2b user only allowed from QGIS or ARCGIS", "code": "C2B_USER_AGENT_NOT_ALLOWED"} if { userAgent := lower(headers["user-agent"]) claims.payload.sub == "c2b" @@ -112,7 +114,12 @@ decision := {"allowed": true, "sub": claims.payload.sub, "kid": claims.kid} if { claims.payload.sub != null } -decision := {"allowed": false, "reason": reason} if { +decision := {"allowed": false, "reason": reason, "sub": sub, "codes": codes} if { count(deny) > 0 - reason := concat(", ", deny) + + reasons := [d.reason | some d in deny] + codes := [d.code | some d in deny] + + reason := concat(", ", reasons) + sub := object.get(claims.payload, "sub", "N/A") } diff --git a/example/data/policy_test.rego b/example/data/policy_test.rego index 4c2f827..f5da4c0 100644 --- a/example/data/policy_test.rego +++ b/example/data/policy_test.rego @@ -86,7 +86,8 @@ test_deny_no_token if { with data.users as users not res.allowed - res.reason == "no token supplied in any of the possible locations" + print(res) + res.reason == "no token supplied in any of the possible locations" } test_deny_malformed_token if { @@ -306,16 +307,6 @@ test_allowed_decision_contains_sub if { res.sub == "avi" } -test_allowed_decision_contains_kid if { - token := generate_token(private_key_1, {"sub": "avi"}) - res := decision with input as {"domain":"avi", "headers": {"Origin": "https://avi.com", "X-Api-Key": token, "User-Agent": chrome_agent}} - with data.keys as public_key_1 - with data.users as users - - res.allowed - res.kid == "opa_test_key_1" -} - # ========================================================= # c2b (client-to-backend) tests # ========================================================= diff --git a/helm/Chart.lock b/helm/Chart.lock index 4bbc240..869ef0b 100644 --- a/helm/Chart.lock +++ b/helm/Chart.lock @@ -1,18 +1,18 @@ dependencies: - name: opa repository: file://charts/opa - version: 1.10.0 + version: 1.12.0 - name: auth-cron repository: file://../packages/auth-cron/helm - version: 1.10.0 + version: 1.12.0 - name: auth-manager repository: file://../packages/auth-manager/helm - version: 1.10.0 + version: 1.12.0 - name: auth-ui repository: file://../packages/auth-ui/helm - version: 1.10.0 + version: 1.12.0 - name: token-kiosk repository: file://../packages/token-kiosk/helm - version: 1.10.0 -digest: sha256:164eb2c4310304fd4d51860fe117047c570e721db34f97747a7a958bef3a9c7a -generated: "2026-01-11T19:22:51.855858+02:00" + version: 1.12.0 +digest: sha256:3ae8431a5a9943c4d5cf1ec92cef60423db6eea085f2f82eabbb6818292da670 +generated: "2026-03-18T15:30:39.641484226+02:00" diff --git a/helm/charts/opa/Chart.lock b/helm/charts/opa/Chart.lock index a81c4cb..a4b431d 100644 --- a/helm/charts/opa/Chart.lock +++ b/helm/charts/opa/Chart.lock @@ -2,5 +2,8 @@ dependencies: - name: mclabels repository: oci://acrarolibotnonprod.azurecr.io/helm/infra version: 1.0.1 -digest: sha256:a97237cd8966ab9d4f8c0b8dda2ad110fbff5d485da868124fdce2a5dbbfa208 -generated: "2025-11-20T10:36:07.259160863+02:00" +- name: fluent-bit + repository: oci://ghcr.io/fluent/helm-charts + version: 0.56.0 +digest: sha256:cc01fd5ba8b22052738e6b6baec3816b5510a09837c44957ef9dca66212e3df8 +generated: "2026-03-17T09:10:00.789970582+02:00" diff --git a/helm/charts/opa/Chart.yaml b/helm/charts/opa/Chart.yaml index 2dafe71..828a285 100644 --- a/helm/charts/opa/Chart.yaml +++ b/helm/charts/opa/Chart.yaml @@ -8,3 +8,8 @@ dependencies: - name: mclabels version: 1.0.1 repository: oci://acrarolibotnonprod.azurecr.io/helm/infra + - name: fluent-bit + repository: oci://ghcr.io/fluent/helm-charts + version: 0.56.0 + condition: fluent-bit.enabled + alias: fluentbit diff --git a/helm/charts/opa/config/opa.yaml b/helm/charts/opa/config/opa.yaml index 256959a..f35d0d6 100644 --- a/helm/charts/opa/config/opa.yaml +++ b/helm/charts/opa/config/opa.yaml @@ -5,7 +5,10 @@ services: s3_signing: # Required for finding the envs for S3 environment_credentials: {} - +{{- if .Values.fluentbit.enabled }} + fluent-bit: + url: {{ .Values.fluentbit.url | default (printf "http://%s-fluentbit:9880" .Release.Name)}} +{{- end }} bundles: authz: service: s3 @@ -21,8 +24,13 @@ status: decision_logs: console: {{ .Values.decisionLogs.console }} + {{- if .Values.fluentbit.enabled }} + service: fluent-bit + {{- end }} reporting: - buffer_size_limit_bytes: {{ .Values.decisionLogs.maxBufferSize }} + buffer_size_limit_bytes: {{ .Values.decisionLogs.maxBufferSize | int }} + min_delay_seconds: {{ .Values.decisionLogs.minDelaySeconds | int }} + max_delay_seconds: {{ .Values.decisionLogs.maxDelaySeconds | int }} {{ if .Values.tracing.enabled }} distributed_tracing: @@ -40,6 +48,7 @@ storage: labels: + environment: {{ .Values.opaEnvironment | required "opaEnvironment is required" | quote }} {{- range $key, $value := .Values.labels }} {{ $key }}: {{ $value | quote }} {{- end }} diff --git a/helm/charts/opa/templates/deployment.yaml b/helm/charts/opa/templates/deployment.yaml index 998bcbd..d553203 100644 --- a/helm/charts/opa/templates/deployment.yaml +++ b/helm/charts/opa/templates/deployment.yaml @@ -36,9 +36,7 @@ spec: {{- end }} annotations: {{ include "mclabels.annotations" . | nindent 8 }} - {{- if .Values.resetOnConfigChange }} checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} - {{- end }} {{- if .Values.additionalPodAnnotations }} {{- toYaml .Values.additionalPodAnnotations | nindent 8}} {{- end }} diff --git a/helm/charts/opa/values.yaml b/helm/charts/opa/values.yaml index 1fa9874..d240eac 100644 --- a/helm/charts/opa/values.yaml +++ b/helm/charts/opa/values.yaml @@ -60,6 +60,8 @@ status: decisionLogs: console: true maxBufferSize: 5242880 # 5MB + min_delay_seconds: 5 + max_delay_seconds: 60 tracing: enabled: false @@ -135,3 +137,142 @@ ingress: # Add any label you want as `KEY: VALUE` labels: {} + +fluentbit: + enabled: true + kind: Deployment + testFramework: + enabled: false + imagePullSecrets: + - name: my-registry-secret + serviceAccount: + create: false + rbac: + create: false + openshift: + enabled: true + resources: + limits: + cpu: 400m + memory: 256Mi + requests: + cpu: 100m + memory: 256Mi + autoscaling: + enabled: true + labels: + mapcolonies.io/environment: dev + mapcolonies.io/component: "infrastructure" + mapcolonies.io/part-of: "monitoring" + mapcolonies.io/owner: "infra" + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "2020" + prometheus.io/path: "/api/v1/metrics/prometheus" + loki: + url: loki.namespace.svc.cluster.local + port: "3100" + envWithTpl: + - name: FLUENT_LOKI_HOST + value: "{{ .Values.loki.url }}" + - name: FLUENT_LOKI_PORT + value: "{{ .Values.loki.port }}" + extraPorts: + - name: input + port: 9880 + containerPort: 9880 + protocol: TCP + luaScripts: + transform.lua: | + function transform_opa_log(tag, timestamp, record) + local new_record = {} + -- 1. Input and Bundles + new_record["input"] = record["input"] + new_record["bundles"] = record["bundles"] + + -- 2. Trace & Span IDs + new_record["trace_id"] = record["trace_id"] + new_record["span_id"] = record["span_id"] + + -- 3. Extract Environment and Domain + if record["labels"] ~= nil then + new_record["env"] = record["labels"]["environment"] + end + + new_record["opa_domain"] = record["input"]["domain"] + + + new_record["token_location"] = "missing" + if record["input"]["headers"] ~= nil and record["input"]["headers"]["x-api-key"] ~= nil then + new_record["token_location"] = "header" + end + + if record["input"]["query"] ~= nil and record["input"]["query"]["token"] ~= nil then + if new_record["token_location"] == "header" then + new_record["token_location"] = "both" + else + new_record["token_location"] = "query" + end + end + + if record["input"]["headers"] ~= nil then + if record["input"]["headers"]["origin"] ~= nil then + new_record["origin"] = record["input"]["headers"]["origin"] + end + if record["input"]["headers"]["user-agent"] ~= nil then + new_record["user_agent"] = record["input"]["headers"]["user-agent"] + end + end + + -- 4. Requested By + new_record["requested_by"] = record["requested_by"] + + -- 5. Result splits + if record["result"] ~= nil then + -- Loki metadata requires strings, so we cast the boolean + new_record["allowed"] = tostring(record["result"]["allowed"]) + new_record["reason"] = record["result"]["reason"] + new_record["sub"] = record["result"]["sub"] + new_record["codes"] = record["result"]["codes"] + end + + -- 6. The original OPA timestamp string + new_record["opa_timestamp"] = record["timestamp"] + + -- 7. Total processing time + if record["metrics"] ~= nil then + new_record["total_time_ns"] = tostring(record["metrics"]["timer_server_handler_ns"]) + end + + -- We return the Fluent Bit ingestion timestamp as the official log time. + return 1, timestamp, new_record + end + + config: + inputs: | + [INPUT] + Name http + Listen 0.0.0.0 + Port 9880 + filters: | + [FILTER] + Name lua + Match * + script /fluent-bit/scripts/transform.lua + call transform_opa_log + outputs: | + [OUTPUT] + Name loki + Match * + Host ${FLUENT_LOKI_HOST} + Port ${FLUENT_LOKI_PORT} + + # LABELS: Domain added here for fast stream routing + Labels job=opa, environment=$env, opa_domain=$opa_domain + + # METADATA: Allowed moved here, along with other high-cardinality data + Structured_metadata trace_id=$trace_id, span_id=$span_id, requested_by=$requested_by, reason=$reason, subject=$sub, total_time_ns=$total_time_ns, allowed=$allowed, token_location=$token_location, origin=$origin, user_agent=$user_agent + + # CLEANUP: Strip these fields from the main JSON log body + Remove_Keys env, domain, allowed, trace_id, span_id, requested_by, reason, sub, total_time_ns, opa_domain, token_location, origin, user_agent +