feat: avatar, account menu, and single sign-on from Go Loose
Signing in now starts at the sign-in screen and one button that hands the browser to Go Loose, which returns to the welcome page. Nothing redirects to the identity provider on its own any more: that fired on every anonymous visit and read as a redirect loop rather than as a choice to sign in. The password form remains only where the API reports that Go Loose login is not configured for the host, because otherwise the application is unreachable on a bare localhost. A person's picture is carried through from the Go Loose identity and rendered in the account menu and on the profile page, with their initials as the fallback. Two things make that fallback necessary rather than decorative: Go Loose retires a picture key when a picture is replaced, so a link cached in a browser can be dead, and most people have not uploaded one. The URL is deliberately not persisted locally, since a stored key would keep failing after the picture changed. The account menu is a disclosure rather than a hover menu: a real button with aria-expanded, Escape and outside clicks closing it, and focus returning to the trigger. It is not an ARIA menu, because its actions are a link and a button and menuitem roles would strip the link semantics from one of them for no gain. The account URL of the Go Loose identity is resolved on each sign-in and is read from the live identity provider every time, which is also what makes it correct after a picture change. Depends on the Go Loose revision that serves avatar_url through /connect/userinfo, so the picture URL reaches this application at all.
This commit is contained in:
parent
6be21597d5
commit
5449cd3d8d
52 changed files with 1927 additions and 434 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
This chart deploys Go Guess as two images, an API and a web front end, plus its
|
||||
own PostgreSQL and RabbitMQ, and publishes one HTTPS host per tenant under
|
||||
`https://<tenant>.guess.local`.
|
||||
`https://<tenant>.guess.urpi.be`.
|
||||
|
||||
## 1. What the chart creates
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ command that exports the CA certificate. With the local platform, prefer
|
|||
`tenants` is the list of slugs and `domain` is the shared parent domain:
|
||||
|
||||
```yaml
|
||||
domain: guess.local
|
||||
domain: guess.urpi.be
|
||||
tenants:
|
||||
- nmbs
|
||||
- ypto
|
||||
|
|
@ -83,8 +83,8 @@ tenants:
|
|||
|
||||
The chart derives, without any extra configuration:
|
||||
|
||||
- `GO_LOOSE_APP_DOMAIN=guess.local`, so the API accepts `nmbs.guess.local` and
|
||||
`ypto.guess.local` as tenant hosts.
|
||||
- `GO_LOOSE_APP_DOMAIN=<domain>`, so the API accepts `nmbs.<domain>` and
|
||||
`ypto.<domain>` as tenant hosts.
|
||||
- One ingress rule per tenant, all pointing at `go-guess-web`.
|
||||
- One certificate SAN per tenant, so a single secret covers every host.
|
||||
- `FRONTEND_URL`, when `api.frontendURL` is empty, as
|
||||
|
|
@ -107,7 +107,7 @@ sign-in is used.
|
|||
|---|---|---|
|
||||
| `nameOverride` | `""` | Overrides the chart name in resource names and labels |
|
||||
| `fullnameOverride` | `""` | Replaces the generated resource name prefix |
|
||||
| `domain` | `guess.local` | Parent domain of every tenant host |
|
||||
| `domain` | `guess.urpi.be` | Parent domain of every tenant host. `guess-dev.urpi.be` in the development profile |
|
||||
| `tenants` | `[nmbs, ypto]` | Tenant slugs served by the release |
|
||||
| `imagePullSecrets` | `[]` | Pull secrets added to both pods |
|
||||
|
||||
|
|
@ -126,8 +126,8 @@ sign-in is used.
|
|||
| `api.service.name` | `api` | Service DNS name, fixed by the web image |
|
||||
| `api.service.type` | `ClusterIP` | API service type |
|
||||
| `api.service.port` | `8080` | API service port |
|
||||
| `api.goLoose.enabled` | `false` | Enables Go Loose browser sign-in |
|
||||
| `api.goLoose.authDomain` | `auth.dev` | Tenant login host `https://<tenant>.<authDomain>` |
|
||||
| `api.goLoose.enabled` | `true` | Enables Go Loose browser sign-in |
|
||||
| `api.goLoose.authDomain` | `auth.urpi.be` | Tenant login host `https://<tenant>.<authDomain>`. `auth-dev.urpi.be` in the development profile |
|
||||
| `api.goLoose.ca.existingSecret` | `""` | Secret holding the Go Loose CA bundle |
|
||||
| `api.goLoose.ca.key` | `ca.crt` | Key projected from that secret |
|
||||
| `api.goLoose.ca.mountPath` | `/certs/local-ca.crt` | `GO_LOOSE_CA_FILE` target |
|
||||
|
|
@ -264,8 +264,8 @@ kubectl -n go-insane-production get ingress go-guess
|
|||
kubectl -n go-insane-production get certificate
|
||||
kubectl -n go-insane-production rollout status deployment/go-guess-api
|
||||
kubectl -n go-insane-production rollout status deployment/go-guess-web
|
||||
curl --resolve nmbs.guess.local:443:127.0.0.1 --cacert go-guess-ca.crt \
|
||||
https://nmbs.guess.local/api/health
|
||||
curl --resolve nmbs.guess.urpi.be:443:127.0.0.1 --cacert go-guess-ca.crt \
|
||||
https://nmbs.guess.urpi.be/api/health
|
||||
```
|
||||
|
||||
The test pod installed by `helm test` checks the API health endpoint and the web
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ app.kubernetes.io/instance: {{ .Release.Name }}
|
|||
{{- default "api" .Values.api.service.name }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Host serving one tenant, for example nmbs.guess.local. */}}
|
||||
{{/* Host serving one tenant, for example nmbs.guess.urpi.be. */}}
|
||||
{{- define "go-guess.tenantHost" -}}
|
||||
{{- printf "%s.%s" (index . 0) (index . 1) }}
|
||||
{{- end }}
|
||||
|
|
@ -81,7 +81,7 @@ app.kubernetes.io/instance: {{ .Release.Name }}
|
|||
{{/* Cluster-scoped by default so one issuer signs every tenant release. */}}
|
||||
{{- define "go-guess.certManagerIssuerRef" -}}
|
||||
{{- if .Values.certManager.issuerRef }}
|
||||
toYaml .Values.certManager.issuerRef
|
||||
{{- toYaml .Values.certManager.issuerRef | nindent 4 }}
|
||||
{{- else }}
|
||||
name: selfsigned
|
||||
kind: Issuer
|
||||
|
|
|
|||
181
deploy/helm/go-guess/values-dev.yaml
Normal file
181
deploy/helm/go-guess/values-dev.yaml
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
# Development values for go-guess
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# Public application domain. Every tenant is served from https://<tenant>.<domain>.
|
||||
domain: guess-dev.urpi.be
|
||||
|
||||
# Tenant slugs. Each one gets its own ingress host and TLS SAN.
|
||||
tenants:
|
||||
- nmbs
|
||||
- ypto
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
api:
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 100
|
||||
runAsGroup: 100
|
||||
fsGroup: 100
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 100
|
||||
runAsGroup: 100
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: batty1039.startdedicated.net:5000/go-guess-api
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
environment: development
|
||||
frontendURL: ""
|
||||
tokenTTL: 8h
|
||||
maxUploadMB: 10
|
||||
service:
|
||||
name: api
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
goLoose:
|
||||
enabled: true
|
||||
authDomain: auth-dev.urpi.be
|
||||
ca:
|
||||
existingSecret: ""
|
||||
key: ca.crt
|
||||
mountPath: /certs/local-ca.crt
|
||||
tenants:
|
||||
nmbs:
|
||||
clientID: ""
|
||||
clientSecret: ""
|
||||
ypto:
|
||||
clientID: ""
|
||||
clientSecret: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 96Mi
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 384Mi
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
strategy:
|
||||
type: Recreate
|
||||
|
||||
web:
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: batty1039.startdedicated.net:5000/go-guess-web
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
nodePort: null
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 96Mi
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
podSecurityContext: {}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
- NET_BIND_SERVICE
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 10m
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
enabled: true
|
||||
existingSecret: ""
|
||||
secretName: ""
|
||||
|
||||
certManager:
|
||||
enabled: true
|
||||
issuerRef:
|
||||
name: letsencrypt-staging
|
||||
kind: ClusterIssuer
|
||||
duration: 8760h
|
||||
renewBefore: 720h
|
||||
caSecretName: ""
|
||||
|
||||
secrets:
|
||||
jwtSecret: ""
|
||||
databaseURL: ""
|
||||
rabbitmqURL: ""
|
||||
postgresqlPassword: ""
|
||||
rabbitmqPassword: ""
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
image:
|
||||
repository: postgres
|
||||
tag: 18-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
username: go_guess
|
||||
database: go_guess
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 2Gi
|
||||
storageClass: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 96Mi
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 384Mi
|
||||
|
||||
rabbitmq:
|
||||
enabled: true
|
||||
image:
|
||||
repository: rabbitmq
|
||||
tag: 4-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
username: go_guess
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 2Gi
|
||||
storageClass: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 96Mi
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 384Mi
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Development profile: demo fixtures, ephemeral database and broker storage, and
|
||||
# `latest` image tags. TLS still uses the chart's self-signed certificate so
|
||||
# https://<tenant>.guess.local behaves like the production profile.
|
||||
domain: guess.local
|
||||
# `latest` image tags. Browser sign-in goes through the development identity
|
||||
# domain auth-dev.urpi.be, never the production auth.urpi.be.
|
||||
domain: guess-dev.urpi.be
|
||||
|
||||
tenants:
|
||||
- nmbs
|
||||
|
|
@ -14,7 +14,7 @@ api:
|
|||
pullPolicy: Always
|
||||
goLoose:
|
||||
enabled: true
|
||||
authDomain: auth.dev
|
||||
authDomain: auth-dev.urpi.be
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
|
|
|
|||
184
deploy/helm/go-guess/values-prd.yaml
Normal file
184
deploy/helm/go-guess/values-prd.yaml
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# Production values for go-guess
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# Public application domain. Every tenant is served from https://<tenant>.<domain>.
|
||||
domain: guess.urpi.be
|
||||
|
||||
# Tenant slugs. Each one gets its own ingress host and TLS SAN.
|
||||
tenants:
|
||||
- nmbs
|
||||
- ypto
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
api:
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 100
|
||||
runAsGroup: 100
|
||||
fsGroup: 100
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 100
|
||||
runAsGroup: 100
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: batty1039.startdedicated.net:5000/go-guess-api
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
environment: production
|
||||
frontendURL: ""
|
||||
tokenTTL: 8h
|
||||
maxUploadMB: 10
|
||||
service:
|
||||
name: api
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
goLoose:
|
||||
enabled: true
|
||||
authDomain: auth.urpi.be
|
||||
ca:
|
||||
existingSecret: ""
|
||||
key: ca.crt
|
||||
mountPath: /certs/local-ca.crt
|
||||
tenants:
|
||||
nmbs:
|
||||
clientID: ""
|
||||
clientSecret: ""
|
||||
ypto:
|
||||
clientID: ""
|
||||
clientSecret: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
web:
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: batty1039.startdedicated.net:5000/go-guess-web
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
nodePort: null
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
podSecurityContext: {}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
- NET_BIND_SERVICE
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 10m
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
enabled: true
|
||||
existingSecret: ""
|
||||
secretName: ""
|
||||
|
||||
certManager:
|
||||
enabled: true
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
duration: 8760h
|
||||
renewBefore: 720h
|
||||
caSecretName: ""
|
||||
|
||||
secrets:
|
||||
jwtSecret: ""
|
||||
databaseURL: ""
|
||||
rabbitmqURL: ""
|
||||
postgresqlPassword: ""
|
||||
rabbitmqPassword: ""
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
image:
|
||||
repository: postgres
|
||||
tag: 18-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
username: go_guess
|
||||
database: go_guess
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 20Gi
|
||||
storageClass: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2Gi
|
||||
|
||||
rabbitmq:
|
||||
enabled: true
|
||||
image:
|
||||
repository: rabbitmq
|
||||
tag: 4-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
username: go_guess
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
storageClass: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2Gi
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Production profile: production fixtures, persistent volumes, released image
|
||||
# tags, and self-signed certificates served for https://<tenant>.guess.local.
|
||||
domain: guess.local
|
||||
# tags, and self-signed certificates served for https://<tenant>.guess.urpi.be.
|
||||
domain: guess.urpi.be
|
||||
|
||||
tenants:
|
||||
- nmbs
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ nameOverride: ""
|
|||
fullnameOverride: ""
|
||||
|
||||
# Public application domain. Every tenant is served from https://<tenant>.<domain>.
|
||||
domain: guess.local
|
||||
domain: guess.urpi.be
|
||||
|
||||
# Tenant slugs. Each one gets its own ingress host and TLS SAN.
|
||||
tenants:
|
||||
|
|
@ -12,6 +12,17 @@ tenants:
|
|||
imagePullSecrets: []
|
||||
|
||||
api:
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 100
|
||||
runAsGroup: 100
|
||||
fsGroup: 100
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 100
|
||||
runAsGroup: 100
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: batty1039.startdedicated.net:5000/go-guess-api
|
||||
|
|
@ -30,8 +41,8 @@ api:
|
|||
type: ClusterIP
|
||||
port: 8080
|
||||
goLoose:
|
||||
enabled: false
|
||||
authDomain: auth.dev
|
||||
enabled: true
|
||||
authDomain: auth.urpi.be
|
||||
ca:
|
||||
# Mount an internal CA bundle and point GO_LOOSE_CA_FILE at it.
|
||||
existingSecret: ""
|
||||
|
|
@ -54,18 +65,6 @@ api:
|
|||
memory: 512Mi
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
# The image runs as its own "app" user (uid 100), so no uid is forced here:
|
||||
# runAsNonRoot then validates whatever the image declares.
|
||||
podSecurityContext: {}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
# The API container applies migrations and seeds before serving, so old pods must
|
||||
# be gone before the new one starts.
|
||||
strategy:
|
||||
type: Recreate
|
||||
|
||||
|
|
|
|||
|
|
@ -55,8 +55,9 @@ curl --fail https://goguess.urpi.be/api/health
|
|||
The `go-guess` service should show `80:31374/TCP`. The PostgreSQL and RabbitMQ
|
||||
services should remain `ClusterIP` services without NodePorts.
|
||||
|
||||
Protected interviewer routes also call Go Loose from the application pod, using
|
||||
`app.goLooseBaseURL` (default `https://%s.auth.dev`, where `%s` is the
|
||||
request tenant). That address must be reachable from the cluster. The browser
|
||||
forwards browser authentication and API requests without adding application
|
||||
credentials. Nginx does not add or strip authorization headers.
|
||||
Browser sign-in also calls Go Loose from the application pod, using
|
||||
`GO_LOOSE_AUTH_DOMAIN` (`auth-dev.urpi.be` outside production, `auth.urpi.be` in
|
||||
production) as `https://<tenant>.<GO_LOOSE_AUTH_DOMAIN>`. That address must be
|
||||
reachable from the pod. The browser forwards browser authentication and API
|
||||
requests without adding application credentials. Nginx does not add or strip
|
||||
authorization headers.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue