Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8178eb2
Forward every value of a repeated query parameter (PER-16922)
zeevmoney Oct 7, 2026
4008d0f
Handle repeated query parameters in /allowed_url rules (PER-16927)
zeevmoney Oct 7, 2026
6af41b1
Tidy pdp-server auth middleware (PER-16926)
zeevmoney Oct 7, 2026
ee1f3ba
Drop the Authorization value from a 401 detail (PER-16926)
zeevmoney Oct 7, 2026
35f16e5
Let a higher-priority mapping rule decide over a conflict (PER-16927)
zeevmoney Oct 7, 2026
3d61732
Read query attributes from the query the rule matched (PER-16927)
zeevmoney Oct 7, 2026
135b31c
Apply the repeated-parameter rule to regex mapping rules (PER-16927)
zeevmoney Oct 7, 2026
bbfe2a6
Capture every log level in the pdp-server test log capture
zeevmoney Oct 7, 2026
30200a2
Use a caret requirement for subtle like the other dependencies
zeevmoney Oct 7, 2026
c68a969
Pin permit-opa with the derivation fix (PER-16921)
zeevmoney Oct 7, 2026
6cfc647
Write OPA's config file when plugins are set (PER-12470)
zeevmoney Oct 7, 2026
0b269a6
Let an explicit local decision-log setting win (PER-16928)
zeevmoney Oct 7, 2026
3d06721
Add nodeSelector, tolerations and affinity to the pdp chart (PER-16923)
zeevmoney Oct 7, 2026
88b401b
Let the pdp chart take the API key from the pod env (PER-16924)
zeevmoney Oct 7, 2026
55c09f6
Bump the pdp chart to 0.0.7
zeevmoney Oct 7, 2026
91b5fdf
Bump docker/scout-action to 1.25.0 (PER-16931)
dependabot[bot] Oct 7, 2026
8ecb566
Bump pre-commit/pre-commit-hooks to v6.0.0 (PER-16931)
dependabot[bot] Oct 7, 2026
2d606aa
Stop Dependabot proposing OPAL 0.9.x past 0.9.6 (PER-16931)
zeevmoney Oct 7, 2026
2f48dd8
Run pdp-tester one at a time against staging (PER-16929)
zeevmoney Oct 7, 2026
f50b276
Rename the Python project from horizon to permit-pdp (PER-16930)
zeevmoney Oct 7, 2026
85e5658
Document building the PDP image with upstream OPA (PER-16934)
zeevmoney Oct 7, 2026
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
19 changes: 15 additions & 4 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
# actionlint configuration.
#
# One suppression, and it is a gap in the linter rather than in the workflow.
# Two suppressions, and both are gaps in the linter rather than in the workflows.
#
# `vulnerability-alerts: read` is the ONLY GITHUB_TOKEN permission that grants
# 1. `vulnerability-alerts: read` is the ONLY GITHUB_TOKEN permission that grants
# GET /repos/{owner}/{repo}/dependabot/alerts. GitHub documents it under the
# `permissions` key in the workflow syntax reference ("For Dependabot alerts, use the
# vulnerability-alerts permission"), but actionlint 1.7.12 still validates permission
# scopes against a hard-coded list that predates it, so it reports the real, working key
# as `unknown permission scope`.
#
# Removal gate: drop this block once actionlint's `AllPermissionScopes` includes
# Removal gate: drop this entry once actionlint's `AllPermissionScopes` includes
# `vulnerability-alerts` — check with
# actionlint .github/workflows/scheduled-security-scan.yml
# after an actionlint upgrade. If it passes, delete this file.
# after an actionlint upgrade.
#
# Deliberately scoped to the one file and the one message. A blanket `permissions` ignore
# would also hide a genuinely misspelled scope, which fails OPEN: an unrecognised scope is
# not granted, the API call 403s, and the watcher reports zero alerts.
#
# 2. `concurrency.queue` (GitHub changelog, 2026-05-07) is what keeps pdp-tester runs waiting
# instead of cancelling each other in tests.yml. actionlint 1.7.12 only knows `group` and
# `cancel-in-progress` (rhysd/actionlint#657), so it reports the documented key as a syntax
# error. Scoped to that file and that message, so a misspelled concurrency key still fails.
#
# Removal gate: drop this entry once `actionlint .github/workflows/tests.yml` passes without
# it after an actionlint upgrade. With both entries gone, delete this file.
paths:
.github/workflows/scheduled-security-scan.yml:
ignore:
- 'unknown permission scope "vulnerability-alerts"'
.github/workflows/tests.yml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ updates:
# exercises end to end. Majors stay manual; minors and patches are fine.
- dependency-name: "websockets"
update-types: ["version-update:semver-major"]
# opal-common and opal-client 0.9.9, the first final release after the 0.9.6 pinned in
# pyproject.toml, declare Requires-Python <3.13 and cannot run on the 3.13 image (see the
# note at that pin). uv ignores Requires-Python upper bounds when it locks, so Dependabot
# would still open the PR and only the pytests job's pip check would catch it. 0.10 stays
# in, so a release that lifts the bound still arrives as a PR. Drop these two once an
# OPAL 0.9.x release supports Python 3.13.
- dependency-name: "opal-common"
versions: [">=0.9.7, <0.10"]
- dependency-name: "opal-client"
versions: [">=0.9.7, <0.10"]
commit-message:
prefix: "deps"
prefix-development: "deps-dev"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: permitio/permit-opa
ref: ff2356d560e3c7152bcb6c6ab8ebd5d4eb691306 # permit-opa 0.0.23 (#52; go 1.26, x/crypto v0.57.0)
ref: 2f411da9b486226a0bfe4c1e39344eaa885553a1 # permit-opa main (permitio/permit-opa#53-#57) + ReBAC derivation fixes (PER-16921); re-pin to the merge commit
path: './permit-opa'
token: ${{ steps.permit-opa-token.outputs.token }}
# Nothing after the clone needs the token; don't leave it in .git/config.
Expand Down Expand Up @@ -347,7 +347,7 @@ jobs:
- name: Docker Scout scan the release image
id: scout
if: ${{ !cancelled() }}
uses: docker/scout-action@7c6b6c3f7844478ace1ffd4e7aef649053d1f87d # v1.24.0
uses: docker/scout-action@221e7f4860634eeb1579e3bd7ca232e577bd1864 # v1.25.0
with:
command: cves
image: local://permitio/pdp-v2:release-scan
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/scheduled-security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ jobs:
# `if:` is skipped once anything earlier in the job has failed, and a skipped report
# leaves code scanning showing yesterday's answer for this image.
if: ${{ !cancelled() }}
uses: docker/scout-action@7c6b6c3f7844478ace1ffd4e7aef649053d1f87d # v1.24.0
uses: docker/scout-action@221e7f4860634eeb1579e3bd7ca232e577bd1864 # v1.25.0
with:
command: cves
# `registry://`, NOT the `local://` that tests.yml uses. There is no local image
Expand All @@ -292,7 +292,7 @@ jobs:
# Load-bearing, not defensive. Without it a failed report step skips this one, and
# the report job would have nothing from Scout to read.
if: ${{ !cancelled() }}
uses: docker/scout-action@7c6b6c3f7844478ace1ffd4e7aef649053d1f87d # v1.24.0
uses: docker/scout-action@221e7f4860634eeb1579e3bd7ca232e577bd1864 # v1.25.0
with:
command: cves
image: registry://permitio/pdp-v2:latest
Expand Down
23 changes: 20 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ jobs:
python -m pip install --dry-run --quiet --disable-pip-version-check \
--require-hashes --no-deps --requirement "$RUNNER_TEMP/lock.txt"

# For horizon/tests/test_helm_chart.py, which renders and lints charts/pdp. Pinned so a
# Helm release cannot change the result on its own; the tests fail, not skip, when CI is set
# and helm is missing.
- name: Install Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v4.3.0

- name: Run Pytests
run: uv run --frozen pytest -s --cache-clear horizon/tests/

Expand Down Expand Up @@ -163,7 +171,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: permitio/permit-opa
ref: ff2356d560e3c7152bcb6c6ab8ebd5d4eb691306 # permit-opa 0.0.23 (#52; go 1.26, x/crypto v0.57.0)
ref: 2f411da9b486226a0bfe4c1e39344eaa885553a1 # permit-opa main (permitio/permit-opa#53-#57) + ReBAC derivation fixes (PER-16921); re-pin to the merge commit
path: './permit-opa'
token: ${{ steps.permit-opa-token.outputs.token }}
# Nothing after the clone needs the token; don't leave it in .git/config.
Expand Down Expand Up @@ -263,6 +271,15 @@ jobs:
pdp-tester:
runs-on: ubuntu-24.04
needs: build-pdp-image
# One run at a time across the repository - PRs, pushes and releases, which reach this
# job through release.yml - because every run writes to the same staging environment and
# overlapping runs failed each other's sync cases (PER-16929). `queue: max` lets up to 100
# runs wait their turn. The default queue holds one, and a newer run cancels the one already
# waiting, which would leave another PR with a cancelled required check. GitHub rejects
# `queue: max` together with cancel-in-progress, so a running test always finishes.
concurrency:
group: pdp-tester-staging
queue: max
# Cap the run so a hung tester fails fast instead of holding the runner
# until GitHub's 360-minute default. The old k3d path was bounded by
# `kubectl wait --timeout=600s`; the tester's own max_running_time only
Expand Down Expand Up @@ -447,7 +464,7 @@ jobs:
username: ${{ vars.DOCKERHUB_ORGANIZATION }}

- name: Docker Scout CVE report (all severities)
uses: docker/scout-action@7c6b6c3f7844478ace1ffd4e7aef649053d1f87d # v1.24.0
uses: docker/scout-action@221e7f4860634eeb1579e3bd7ca232e577bd1864 # v1.25.0
with:
command: cves
image: local://permitio/pdp-v2:next
Expand All @@ -473,7 +490,7 @@ jobs:

- name: Docker Scout CVE gate (high and critical)
id: scout-gate
uses: docker/scout-action@7c6b6c3f7844478ace1ffd4e7aef649053d1f87d # v1.24.0
uses: docker/scout-action@221e7f4860634eeb1579e3bd7ca232e577bd1864 # v1.25.0
with:
command: cves
image: local://permitio/pdp-v2:next
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ PDP_CONTROL_PLANE=https://api.permit.io PDP_API_KEY=<YOUR_API_KEY> uv run uvicor
```

## Building a Custom PDP Docker image
The build compiles Permit's OPA build from the private `permitio/permit-opa` repository, which
`build_opal_bundle.sh` clones over SSH into `../permit-opa`.

For ARM architecture:
```
VERSION=<TAG> make build-arm64
Expand All @@ -64,6 +67,16 @@ For AMD64 architecture:
VERSION=<TAG> make build-amd64
```

### Building without access to permit-opa
`PDP_VANILLA=true` builds the image with upstream OPA instead (`OPA_BUILD=vanilla`), for
development without access to `permitio/permit-opa`. It works with every build target:
```
PDP_VANILLA=true VERSION=<TAG> make build
```
Permit-generated policies call builtins that exist only in Permit's OPA build, so an image built
this way cannot evaluate them. To evaluate Permit policies, use the published `permitio/pdp-v2`
image.

### Running the image in development mode
```
VERSION=<TAG> API_KEY=<PDP_API_KEY> make run
Expand Down
2 changes: 1 addition & 1 deletion charts/pdp/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: v2
name: pdp
description: An official Helm chart for Permit.io PDP (Policy Decision Point) with OpenShift support and configurable ports
version: 0.0.6
version: 0.0.7
keywords:
- policy
- authorization
Expand Down
13 changes: 13 additions & 0 deletions charts/pdp/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ Common labels
{{- end }}
{{- end }}

{{/*
Fail on API key settings that contradict each other or that a template would misread
*/}}
{{- define "pdp.validateApiKeySource" -}}
{{- $userProvidedSecret := .Values.pdp.userProvidedSecret | default false -}}
{{- if not (kindIs "bool" $userProvidedSecret) -}}
{{- fail (printf "pdp.userProvidedSecret must be true or false, got the %s %q. A quoted \"false\" counts as set and would drop PDP_API_KEY." (kindOf $userProvidedSecret) (toString $userProvidedSecret)) -}}
{{- end -}}
{{- if and $userProvidedSecret .Values.pdp.existingApiKeySecret -}}
{{- fail "pdp.userProvidedSecret and pdp.existingApiKeySecret cannot both be set: existingApiKeySecret makes the chart read PDP_API_KEY from that Secret, userProvidedSecret makes it read no Secret at all. Unset one of them." -}}
{{- end -}}
{{- end }}

{{/*
Get the secret name for the API key
*/}}
Expand Down
15 changes: 15 additions & 0 deletions charts/pdp/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{- include "pdp.validateApiKeySource" . }}
apiVersion: apps/v1
kind: Deployment
metadata:
Expand Down Expand Up @@ -50,11 +51,13 @@ spec:
containerPort: {{ .targetPort }}
{{- end }}
env:
{{- if not .Values.pdp.userProvidedSecret }}
- name: PDP_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "pdp.secretName" . }}
key: {{ include "pdp.secretKey" . }}
{{- end }}
{{- if .Values.pdp.pdpEnvs }}
{{- range .Values.pdp.pdpEnvs }}
- name: {{ .name }}
Expand Down Expand Up @@ -155,3 +158,15 @@ spec:
- name: opa-volume
emptyDir: {}
{{- end }}
{{- with .Values.pdp.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.pdp.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.pdp.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
2 changes: 1 addition & 1 deletion charts/pdp/templates/secret.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{- if not .Values.pdp.existingApiKeySecret }}
{{- if not (or .Values.pdp.existingApiKeySecret .Values.pdp.userProvidedSecret) }}
apiVersion: v1
kind: Secret
metadata:
Expand Down
36 changes: 36 additions & 0 deletions charts/pdp/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,27 @@ pdp:
# Example - enable Envoy gRPC ext_authz on port 9191 (requires PDP >= 0.9.10):
# - name: PDP_OPA_PLUGINS
# value: '{"permit_graph":{},"envoy_ext_authz_grpc":{"addr":":9191","path":"permit/root"}}'
# The PDP's API key. By default the chart stores ApiKey in a Secret it creates
# (permitio-pdp-secret) and passes it to the PDP as PDP_API_KEY. existingApiKeySecret and
# userProvidedSecret below change that. Set at most one of them: with both set, the chart
# fails to render.
ApiKey: "<your PDP API Key>"

# Use an existing secret for the API key instead of creating one
# If defined, the chart will not create a secret and will use this existing secret
# existingApiKeySecret:
# name: "my-existing-secret"
# key: "api-key"

# Set to true when something other than the chart supplies PDP_API_KEY, for example a
# mutating webhook such as bank-vaults. The chart then creates no Secret, sets no PDP_API_KEY
# and ignores ApiKey. Set PDP_API_KEY in pdpEnvs above, or have the webhook add it to the pod.
# pdpEnvs values are stored in the Deployment as plain text, so put a reference there that
# the webhook resolves when the container starts (for example a bank-vaults `vault:` path),
# and keep a literal key in ApiKey or existingApiKeySecret. Must be a boolean: a quoted
# "false" fails the render. Without PDP_API_KEY the PDP does not start: its health checks
# fail and Kubernetes keeps restarting the container.
userProvidedSecret: false
port: 7766
# Example - expose Envoy gRPC ext_authz port (requires PDP_OPA_PLUGINS env var above):
# additionalPorts:
Expand All @@ -43,6 +57,28 @@ pdp:
index: "<elasticsearch index>"
debug_mode: false

# Pod scheduling, copied into the PDP pod spec as given. Empty values render nothing.
# Example - run only on a dedicated, tainted node pool:
# nodeSelector:
# pool: pdp
# tolerations:
# - key: "dedicated"
# operator: "Equal"
# value: "pdp"
# effect: "NoSchedule"
# affinity:
# podAntiAffinity:
# preferredDuringSchedulingIgnoredDuringExecution:
# - weight: 100
# podAffinityTerm:
# topologyKey: kubernetes.io/hostname
# labelSelector:
# matchLabels:
# app: permitio-pdp
nodeSelector: {}
tolerations: []
affinity: {}

podDisruptionBudget:
# Automatically enabled when replicas > 1
# Set minAvailable OR maxUnavailable (not both)
Expand Down
8 changes: 5 additions & 3 deletions horizon/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,13 +245,15 @@ def __new__(cls, *, prefix=None, is_model=True): # noqa: ARG004
OPA_DECISION_LOG_ENABLED = confi.bool(
"OPA_DECISION_LOG_ENABLED",
True,
description="if true, OPA decision logs will be uploaded to the Permit.io cloud console",
description="if true, OPA decision logs will be uploaded to the Permit.io cloud console. "
"The control plane also sends this setting; a value set in the PDP's environment takes "
"precedence over it",
)
OPA_DECISION_LOG_CONSOLE = confi.bool(
"OPA_DECISION_LOG_CONSOLE",
False,
description="if true, OPA decision logs will also be printed to console "
"(only relevant if `OPA_DECISION_LOG_ENABLED` is true)",
description="if true, OPA decision logs will be printed to console, "
"whether or not `OPA_DECISION_LOG_ENABLED` uploads them",
)
OPA_DECISION_LOG_INGRESS_ROUTE = confi.str(
"OPA_DECISION_LOG_INGRESS_ROUTE",
Expand Down
Loading
Loading