Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions .agents/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ editing templates or values.
`authup.tplvalues.render`, so umbrella charts can inject template
expressions (the PrivateAIM lesson: their untemplatable `existingSecret`
forced a hardcoded-names table).
17. **`global` must stay open in the schema.** helm copies a parent chart's
ENTIRE `global` map into every subchart before validating that subchart's
schema, so `additionalProperties: false` there makes the chart
uninstallable as a dependency of any umbrella that sets a global this
chart does not declare. `values.yaml` carries the
`# @schema additionalProperties: true` opt-out and `ci/default-values.yaml`
a stray global key as the regression guard. The chart reads only
`imageRegistry` / `imagePullSecrets` / `defaultStorageClass` and ignores
the rest.
18. **An HTTPRoute rule with no `matches` is a catch-all.** The Gateway API
defaults an empty `matches` to PathPrefix `/`, and route hostnames come
from the public URL's ORIGIN (the path is dropped), so a sub-path
deployment would silently take over the whole shared hostname.
`validations.yaml` fails that combination; `route.matches` / `route.filters`
are the raw passthroughs that express it (authup always serves at `/`, so
the prefix must be matched AND rewritten away).

## Values conventions

Expand Down
13 changes: 13 additions & 0 deletions .agents/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,21 @@ helm template t charts/authup --set server.theme.enabled=true --set server.theme
helm template t charts/authup --set server.theme.enabled=true --set server.theme.logo=assets/logo.svg # asset missing from files
helm template t charts/authup --set server.theme.enabled=true --set 'server.theme.tokens.--authup-bg=url(x)' # token value authup rejects
helm template t charts/authup --set 'server.trustedOrigins[0]=https://**.x' # globstar host
helm template t charts/authup --set server.route.enabled=true --set server.publicUrl=https://h.x/auth # sub-path route without matches
helm template t charts/authup --set server.route.enabled=true --set server.ingress.enabled=true \
--set server.ingress.hostname=h.x --set server.ingress.path=/auth # same, via the derived URL
```

The route guard reads the public URL AFTER derivation, so the ingress-derived
case needs its own line: only the origin reaches the HTTPRoute hostname, and the
dropped path is exactly what turns the rule into a catch-all. Adding
`--set 'server.route.matches[0].path.value=/auth'` must make both RENDER.

Umbrella use is part of the contract: `global` must stay open. Render a throwaway
parent chart with authup in `charts/` and an unrelated global (`global.myOrgKey`)
whenever the schema generation changes; `ci/default-values.yaml` carries a stray
global key as the cheap in-repo version of that check.

A single `*` host wildcard (`https://*.example.com`) must still RENDER: authup
supports it, only `**` is the allow-any-origin trap.

Expand Down
10 changes: 4 additions & 6 deletions charts/authup/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ annotations:
- name: Source
url: https://github.com/authup/helm
artifacthub.io/changes: |
- kind: changed
description: appVersion tracks authup 1.0.0-beta.62
- kind: fixed
description: A parent chart's own global.* keys no longer fail schema validation
- kind: added
description: server.theme manifest values compose theme.json (title, logo, tokens)
description: server.route / adminConsole.route accept matches and filters
- kind: added
description: server.features.accountConsole toggles the /account self-service console
- kind: added
description: Trusted origins carrying "**" now fail the render, as authup fails the boot
description: An HTTPRoute for a sub-path public URL without matches now fails the render
12 changes: 8 additions & 4 deletions charts/authup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ Kubernetes: `>=1.25.0-0`
| adminConsole.ingress.enabled | bool | `false` | Enable ingress for the UI |
| adminConsole.ingress.extraHosts | list | `[]` | Extra hosts |
| adminConsole.ingress.extraPaths | list | `[]` | Extra paths for the primary host |
| adminConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; overrides the generated rule) |
| adminConsole.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) |
| adminConsole.ingress.extraTls | list | `[]` | Extra TLS entries |
| adminConsole.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered); also drives the derived UI public URL |
| adminConsole.ingress.ingressClassName | string | `""` | Ingress class name |
Expand Down Expand Up @@ -231,7 +231,9 @@ Kubernetes: `>=1.25.0-0`
| adminConsole.revisionHistoryLimit | int | `3` | Deployment revision history limit |
| adminConsole.route.annotations | object | `{}` | HTTPRoute annotations |
| adminConsole.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for the UI |
| adminConsole.route.hostnames | list | `[]` | Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname) |
| adminConsole.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix |
| adminConsole.route.hostnames | list | `[]` | Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname; only the host is kept, a public URL path is dropped and needs its own matches entry) |
| adminConsole.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" |
| adminConsole.route.parentRefs | list | `[]` | Gateway parentRefs |
| adminConsole.schedulerName | string | `""` | Scheduler name |
| adminConsole.service.annotations | object | `{}` | Service annotations (tpl-rendered) |
Expand Down Expand Up @@ -369,7 +371,7 @@ Kubernetes: `>=1.25.0-0`
| server.ingress.enabled | bool | `false` | Enable ingress for server-core. NOTE: this also exposes the UNAUTHENTICATED /metrics endpoint publicly — block it at the ingress controller or disable it via server.configuration ("middlewarePrometheus: false") when it is not scraped |
| server.ingress.extraHosts | list | `[]` | Extra hosts |
| server.ingress.extraPaths | list | `[]` | Extra paths for the primary host |
| server.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; overrides the generated rule) |
| server.ingress.extraRules | list | `[]` | Full custom rules (tpl-rendered; appended after the generated rules) |
| server.ingress.extraTls | list | `[]` | Extra TLS entries |
| server.ingress.hostname | string | `""` | Ingress hostname (tpl-rendered); also drives the derived PUBLIC_URL |
| server.ingress.ingressClassName | string | `""` | Ingress class name |
Expand Down Expand Up @@ -433,7 +435,9 @@ Kubernetes: `>=1.25.0-0`
| server.revisionHistoryLimit | int | `3` | Deployment revision history limit |
| server.route.annotations | object | `{}` | HTTPRoute annotations |
| server.route.enabled | bool | `false` | Create a Gateway API HTTPRoute for server-core |
| server.route.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl / ingress hostname) |
| server.route.filters | list | `[]` | Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix |
| server.route.hostnames | list | `[]` | Route hostnames ([] = derived from server.publicUrl / ingress hostname; only the host is kept, a public URL path is dropped and needs its own matches entry) |
| server.route.matches | list | `[]` | Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/" |
| server.route.parentRefs | list | `[]` | Gateway parentRefs |
| server.schedulerName | string | `""` | Scheduler name |
| server.service.annotations | object | `{}` | Service annotations (tpl-rendered) |
Expand Down
5 changes: 5 additions & 0 deletions charts/authup/ci/default-values.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Baseline: built-in PostgreSQL, both services.
# The stray global key is a regression guard: helm copies a parent chart's whole
# global map into every subchart before schema validation, so the schema must not
# reject globals this chart does not declare.
global:
umbrellaProbe: parent-injected
postgresql:
persistence:
enabled: false
Expand Down
16 changes: 16 additions & 0 deletions charts/authup/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ small deployments. For production, consider an external database
(externalDatabase.*) or an operator (e.g. CloudNativePG).
{{- end }}

{{- $prefixed := list }}
{{- if and .Values.server.enabled .Values.server.route.enabled (urlParse $serverUrl).path (ne (urlParse $serverUrl).path "/") }}
{{- $prefixed = append $prefixed (printf "server.route %s" (urlParse $serverUrl).path) }}
{{- end }}
{{- if and .Values.adminConsole.enabled .Values.adminConsole.route.enabled (urlParse $uiUrl).path (ne (urlParse $uiUrl).path "/") }}
{{- $prefixed = append $prefixed (printf "adminConsole.route %s" (urlParse $uiUrl).path) }}
{{- end }}
{{- if $prefixed }}

NOTE: serving under a path prefix ({{ join ", " $prefixed }}). authup itself always
serves at "/", so something in the chain has to strip the prefix: a route.filters
URLRewrite (ReplacePrefixMatch "/"), an implementation-specific filter, or a proxy
in front of the gateway. Without one, every request reaches the pod with the prefix
still attached and 404s.
{{- end }}

{{- if $serverUrl }}

Changing PUBLIC_URL later breaks enrolled WebAuthn credentials (the rpId binds
Expand Down
6 changes: 6 additions & 0 deletions charts/authup/templates/_ingress.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,10 @@ spec:
- backendRefs:
- name: {{ .serviceName }}
port: {{ .servicePort }}
{{- if $route.matches }}
matches: {{- include "authup.tplvalues.render" (dict "value" $route.matches "context" $ctx) | nindent 8 }}
{{- end }}
{{- if $route.filters }}
filters: {{- include "authup.tplvalues.render" (dict "value" $route.filters "context" $ctx) | nindent 8 }}
{{- end }}
{{- end -}}
20 changes: 20 additions & 0 deletions charts/authup/templates/validations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ store, token blocklist and MFA challenges fall back to a per-process memory cach
{{- fail "authup: adminConsole.ingress.enabled requires adminConsole.ingress.hostname (or extraHosts / extraRules)." }}
{{- end }}

{{/*
A Gateway API HTTPRoute rule without `matches` defaults to PathPrefix "/", i.e. it
matches every request on every hostname the route attaches to. The route hostname is
derived from the public URL's ORIGIN, so a sub-path public URL would silently attach
authup as the catch-all backend of a hostname it is meant to share. authup always
serves at "/" and expects the proxy to strip the prefix, hence match AND rewrite.
*/}}
{{- if and .Values.server.enabled .Values.server.route.enabled (not .Values.server.route.matches) }}
{{- $path := (urlParse (include "authup.server.publicUrl" .)).path }}
{{- if and $path (ne $path "/") }}
{{- fail (printf "authup: the server public URL carries the path %q but server.route.matches is empty, which the Gateway API defaults to PathPrefix \"/\" (a catch-all on every hostname this route attaches to). authup serves at \"/\" and expects the proxy to strip the prefix, so set both:\n server.route.matches: [{path: {type: PathPrefix, value: %s}}]\n server.route.filters: [{type: URLRewrite, urlRewrite: {path: {type: ReplacePrefixMatch, replacePrefixMatch: /}}}]" $path $path) }}
{{- end }}
{{- end }}
{{- if and .Values.adminConsole.enabled .Values.adminConsole.route.enabled (not .Values.adminConsole.route.matches) }}
{{- $path := (urlParse (include "authup.adminConsole.publicUrl" .)).path }}
{{- if and $path (ne $path "/") }}
{{- fail (printf "authup: the admin console public URL carries the path %q but adminConsole.route.matches is empty, which the Gateway API defaults to PathPrefix \"/\" (a catch-all on every hostname this route attaches to). Set both:\n adminConsole.route.matches: [{path: {type: PathPrefix, value: %s}}]\n adminConsole.route.filters: [{type: URLRewrite, urlRewrite: {path: {type: ReplacePrefixMatch, replacePrefixMatch: /}}}]" $path $path) }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{{- end }}
{{- end }}

{{/* URL values must carry a scheme — a scheme-less value would derive a broken "://" origin into TRUSTED_ORIGINS and crash-loop server-core. */}}
{{- range $key, $value := dict "server.publicUrl" .Values.server.publicUrl "adminConsole.publicUrl" .Values.adminConsole.publicUrl "adminConsole.apiUrl" .Values.adminConsole.apiUrl "adminConsole.internalApiUrl" .Values.adminConsole.internalApiUrl }}
{{- if and $value (not (contains "{{" $value)) (not (regexMatch "^https?://" $value)) }}
Expand Down
55 changes: 47 additions & 8 deletions charts/authup/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@
"type": "array"
},
"extraRules": {
"description": "Full custom rules (tpl-rendered; overrides the generated rule)",
"description": "Full custom rules (tpl-rendered; appended after the generated rules)",
"items": {
"required": []
},
Expand Down Expand Up @@ -742,15 +742,33 @@
"title": "enabled",
"type": "boolean"
},
"filters": {
"description": "Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix",
"items": {
"required": []
},
"required": [],
"title": "filters",
"type": "array"
},
"hostnames": {
"description": "Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname)",
"description": "Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname; only the host is kept, a public\nURL path is dropped and needs its own matches entry)",
"items": {
"required": []
},
"required": [],
"title": "hostnames",
"type": "array"
},
"matches": {
"description": "Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix \"/\"",
"items": {
"required": []
},
"required": [],
"title": "matches",
"type": "array"
},
"parentRefs": {
"description": "Gateway parentRefs",
"items": {
Expand All @@ -764,7 +782,9 @@
"required": [
"enabled",
"hostnames",
"parentRefs"
"parentRefs",
"matches",
"filters"
],
"title": "route",
"type": "object"
Expand Down Expand Up @@ -1359,7 +1379,7 @@
"type": "string"
},
"global": {
"additionalProperties": false,
"additionalProperties": true,
"properties": {
"defaultStorageClass": {
"default": "",
Expand Down Expand Up @@ -2469,7 +2489,7 @@
"type": "array"
},
"extraRules": {
"description": "Full custom rules (tpl-rendered; overrides the generated rule)",
"description": "Full custom rules (tpl-rendered; appended after the generated rules)",
"items": {
"required": []
},
Expand Down Expand Up @@ -3095,15 +3115,33 @@
"title": "enabled",
"type": "boolean"
},
"filters": {
"description": "Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix",
"items": {
"required": []
},
"required": [],
"title": "filters",
"type": "array"
},
"hostnames": {
"description": "Route hostnames ([] = derived from server.publicUrl / ingress hostname)",
"description": "Route hostnames ([] = derived from server.publicUrl / ingress hostname; only the host is kept, a public\nURL path is dropped and needs its own matches entry)",
"items": {
"required": []
},
"required": [],
"title": "hostnames",
"type": "array"
},
"matches": {
"description": "Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix \"/\"",
"items": {
"required": []
},
"required": [],
"title": "matches",
"type": "array"
},
"parentRefs": {
"description": "Gateway parentRefs",
"items": {
Expand All @@ -3117,7 +3155,9 @@
"required": [
"enabled",
"hostnames",
"parentRefs"
"parentRefs",
"matches",
"filters"
],
"title": "route",
"type": "object"
Expand Down Expand Up @@ -3890,7 +3930,6 @@
}
},
"required": [
"global",
"nameOverride",
"fullnameOverride",
"namespaceOverride",
Expand Down
22 changes: 18 additions & 4 deletions charts/authup/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## @section Global parameters

# @schema
# type: object
# additionalProperties: true
# @schema
global:
# -- Global container image registry override (takes precedence over image.registry)
imageRegistry: ""
Expand Down Expand Up @@ -739,12 +743,13 @@ server:
extraPaths: []
# -- Extra TLS entries
extraTls: []
# -- Full custom rules (tpl-rendered; overrides the generated rule)
# -- Full custom rules (tpl-rendered; appended after the generated rules)
extraRules: []
route:
# -- Create a Gateway API HTTPRoute for server-core
enabled: false
# -- Route hostnames ([] = derived from server.publicUrl / ingress hostname)
# -- Route hostnames ([] = derived from server.publicUrl / ingress hostname; only the host is kept, a public
# URL path is dropped and needs its own matches entry)
hostnames: []
# -- Gateway parentRefs
parentRefs: []
Expand All @@ -753,6 +758,10 @@ server:
# @schema
# -- HTTPRoute annotations
annotations: {}
# -- Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/"
matches: []
# -- Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix
filters: []
metrics:
# -- The /metrics endpoint is UNAUTHENTICATED; keep it off the public ingress
serviceMonitor:
Expand Down Expand Up @@ -1040,12 +1049,13 @@ adminConsole:
extraPaths: []
# -- Extra TLS entries
extraTls: []
# -- Full custom rules (tpl-rendered; overrides the generated rule)
# -- Full custom rules (tpl-rendered; appended after the generated rules)
extraRules: []
route:
# -- Create a Gateway API HTTPRoute for the UI
enabled: false
# -- Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname)
# -- Route hostnames ([] = derived from adminConsole.publicUrl / ingress hostname; only the host is kept, a public
# URL path is dropped and needs its own matches entry)
hostnames: []
# -- Gateway parentRefs
parentRefs: []
Expand All @@ -1054,6 +1064,10 @@ adminConsole:
# @schema
# -- HTTPRoute annotations
annotations: {}
# -- Rule matches (tpl-rendered); [] is the Gateway API default, PathPrefix "/"
matches: []
# -- Rule filters (tpl-rendered), e.g. a URLRewrite stripping a path prefix
filters: []
pdb:
# -- Create a PodDisruptionBudget for the UI
create: false
Expand Down
Loading