v0.7.60-beta - app-instellingen nagelopen op fouten bij het installeren
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m27s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m27s
Aanleiding: Homepage weigerde met "Host validation failed" omdat de standaardwaarde van allowed_hosts poort 3001 noemde terwijl het poortveld op 3002 stond. Alle 96 sjablonen zijn daarop nagelopen. Systemisch: - De dobbelsteenknop, waar zeventien apps in hun uitleg naar verwijzen, verscheen nooit: hij hing af van een veld 'secret' dat fields() niet meestuurde. Wachtwoorden stonden daardoor leesbaar in het formulier. - 27 velden die je toch nooit zelf typt (databasewachtwoorden, JWT- en versleutelingssleutels) worden nu voorgevuld met een willekeurige waarde. Inlogwachtwoorden krijgen alleen een knop, want die moet je noteren. - De knop hangt nu aan 'generate' in plaats van aan 'secret': voor een WireGuard-privésleutel of een token uit een andere app is een verzonnen waarde juist fout. - required werd nergens gecontroleerd. Je kon installeren met een leeg databasewachtwoord of een lege sleutel. Nu geweigerd bij installeren en bij herconfigureren, met vermelding van de lege velden. Velden achter een uitgeschakelde groepsschakelaar tellen niet mee. - Wachtwoorden werden uitgeschreven in het installatielog; nu gemaskeerd. Losse fouten: - homepage: allowed_hosts stond op localhost:3001, nu * met uitleg. - baserow, hedgedoc, ntfy: URL in de standaardwaarde wees naar een poort waar niets luistert. - Vast wachtwoord in acht sjablonen weggehaald: ghost, immich, miniflux, paperless-ngx, unifi-network en vikunja kregen allemaal dezelfde database-wachtwoorden; grafana en gotify stonden op 'admin'. - data_dir bij keycloak, metube, miniflux en teslamate stond in het formulier maar kwam nergens terecht (die apps gebruiken een Docker-volume). - puid/pgid bij freshrss: het image kent ze niet. - prometheus gaf de gevraagde tijdzone niet door. Nieuw tests/test_app_templates.py: rendert elke app met zijn standaardwaarden en controleert ongedefinieerde variabelen, velden zonder werking, niet-gedeclareerde volumes, depends_on, dubbele containernamen, poortconflicten, poortnummers in standaardwaarden en vaste wachtwoorden. 2236 tests groen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
This commit is contained in:
parent
87fb9d19d1
commit
952e7ddb7a
42 changed files with 714 additions and 103 deletions
56
CHANGELOG.md
56
CHANGELOG.md
|
|
@ -1,3 +1,59 @@
|
||||||
|
# v0.7.60-beta — App-instellingen die niet meer stilzwijgend misgaan
|
||||||
|
|
||||||
|
Aanleiding: Homepage weigerde na installatie met **"Host validation failed"**.
|
||||||
|
De standaardwaarde van `allowed_hosts` noemde poort 3001 terwijl het poortveld
|
||||||
|
op 3002 stond — en `localhost` werkt sowieso niet als je je server op zijn
|
||||||
|
IP-adres benadert. Dat bleek geen los geval, dus alle 96 sjablonen zijn
|
||||||
|
nagelopen.
|
||||||
|
|
||||||
|
**De dobbelsteenknop bestond niet.** Zeventien apps zeggen in hun uitleg
|
||||||
|
"gebruik de dobbelsteenknop voor een willekeurige waarde". Die knop hing af van
|
||||||
|
een veld `secret` dat de backend nooit meestuurde, dus hij verscheen nooit — en
|
||||||
|
wachtwoorden stonden als gewone leesbare tekst in het formulier. Nu worden
|
||||||
|
geheimen herkend, en 27 velden die je toch nooit zelf typt (databasewachtwoorden,
|
||||||
|
JWT- en versleutelingssleutels) worden meteen ingevuld met een willekeurige
|
||||||
|
waarde. Inlogwachtwoorden krijgen alleen de knop: die moet je zelf noteren.
|
||||||
|
|
||||||
|
De knop hangt nu aan "hier past een willekeurige waarde" en niet aan "dit is
|
||||||
|
geheim" — voor een WireGuard-privésleutel of een token uit een andere app is
|
||||||
|
iets verzonnens juist fout.
|
||||||
|
|
||||||
|
**Verplichte velden werden niet gecontroleerd.** Het formulier zette een
|
||||||
|
sterretje achter een verplicht veld en hield verder niets tegen: je kon een app
|
||||||
|
installeren met een leeg databasewachtwoord of een lege sleutel. De container
|
||||||
|
start dan niet, of draait met een leeg geheim. Dat wordt nu geweigerd, bij
|
||||||
|
installeren én bij het achteraf wijzigen van instellingen, met de melding welke
|
||||||
|
velden nog leeg zijn. Velden achter een uitgeschakelde groepsschakelaar tellen
|
||||||
|
niet mee.
|
||||||
|
|
||||||
|
**Acht apps hadden een vast wachtwoord in het sjabloon**: `immich_db_pass`,
|
||||||
|
`ghost_db_pass` en soortgelijke voor iedereen die het installeert, en `admin` als
|
||||||
|
beheerderswachtwoord van Grafana en Gotify. De databasewachtwoorden worden nu
|
||||||
|
gegenereerd; bij Grafana en Gotify moet je zelf een wachtwoord kiezen.
|
||||||
|
|
||||||
|
**Wachtwoorden stonden in het installatielog**, dat in de interface zichtbaar is
|
||||||
|
en vaak in een bugmelding geplakt wordt. Die worden nu gemaskeerd.
|
||||||
|
|
||||||
|
Verder rechtgezet:
|
||||||
|
|
||||||
|
- `baserow`, `hedgedoc` en `ntfy` hadden dezelfde poortfout als Homepage: de
|
||||||
|
URL in de standaardwaarde wees naar een poort waar niets luistert, waardoor
|
||||||
|
links en terugkeer-na-inloggen niet werkten.
|
||||||
|
- Zes velden stonden in het formulier maar kwamen nergens terecht: `data_dir`
|
||||||
|
bij Keycloak, MeTube, Miniflux en TeslaMate (die gebruiken een Docker-volume,
|
||||||
|
dus je gegevens stonden niet waar je dacht), en `puid`/`pgid` bij FreshRSS,
|
||||||
|
die het image helemaal niet kent.
|
||||||
|
- De tijdzone van Prometheus werd wel gevraagd maar niet doorgegeven.
|
||||||
|
|
||||||
|
Nieuw is `tests/test_app_templates.py`: elke app wordt met zijn
|
||||||
|
standaardwaarden gerenderd en gecontroleerd op ongedefinieerde variabelen,
|
||||||
|
velden zonder werking, niet-gedeclareerde volumes, `depends_on` naar een
|
||||||
|
onbekende service, dubbele containernamen, poortconflicten binnen een stack,
|
||||||
|
poortnummers in standaardwaarden die niet kloppen met het poortveld, en vaste
|
||||||
|
wachtwoorden. 2236 tests groen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# v0.7.50-beta — Je hoort het als er iets misgaat
|
# v0.7.50-beta — Je hoort het als er iets misgaat
|
||||||
|
|
||||||
Het onbewaakte deel was de zwakke plek. Geplande backups, updatechecks en de
|
Het onbewaakte deel was de zwakke plek. Geplande backups, updatechecks en de
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.7.50-beta
|
0.7.60-beta
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "session_secret",
|
"name": "session_secret",
|
||||||
|
|
@ -73,7 +75,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "storage_key",
|
"name": "storage_key",
|
||||||
|
|
@ -85,7 +89,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,9 @@
|
||||||
"description": "Willekeurige waarde voor sessieversleuteling; gebruik de dobbelsteenknop",
|
"description": "Willekeurige waarde voor sessieversleuteling; gebruik de dobbelsteenknop",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "willekeurige-lange-string"
|
"placeholder": "willekeurige-lange-string"
|
||||||
}
|
},
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "allowed_hosts",
|
"name": "allowed_hosts",
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,10 @@
|
||||||
"name": "public_url",
|
"name": "public_url",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Publieke URL",
|
"title": "Publieke URL",
|
||||||
"default": "http://localhost:8098",
|
"default": "http://localhost:8099",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "Zonder dit kloppen links en uploads niet"
|
"description": "Zonder dit kloppen links en uploads niet",
|
||||||
|
"help": "Het adres waarop je Baserow opent. Baserow bouwt hier zijn links mee op, dus een verkeerde poort levert knoppen op die nergens heen gaan. Benader je de server op zijn IP-adres, vul dan dat adres in: http://10.0.20.5:8099"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "app_url",
|
"name": "app_url",
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "app_key",
|
"name": "app_key",
|
||||||
|
|
@ -73,7 +75,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "32-tekens-lange-sleutel"
|
"placeholder": "32-tekens-lange-sleutel"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,21 +50,6 @@
|
||||||
"title": "Tijdzone",
|
"title": "Tijdzone",
|
||||||
"default": "Europe/Amsterdam",
|
"default": "Europe/Amsterdam",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "puid",
|
|
||||||
"type": "int",
|
|
||||||
"title": "Gebruikers-ID (PUID)",
|
|
||||||
"default": 1000,
|
|
||||||
"required": true,
|
|
||||||
"description": "Bepaalt de eigenaar van de bestanden; `id -u` op je host"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pgid",
|
|
||||||
"type": "int",
|
|
||||||
"title": "Groeps-ID (PGID)",
|
|
||||||
"default": 1000,
|
|
||||||
"required": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,11 +70,14 @@
|
||||||
"name": "db_password",
|
"name": "db_password",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Database wachtwoord",
|
"title": "Database wachtwoord",
|
||||||
"default": "ghost_db_pass",
|
"default": "",
|
||||||
"required": true,
|
"required": true,
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"secret": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"help": "Wordt automatisch ingevuld met een willekeurige waarde. Alleen de containers onderling gebruiken hem, dus je hoeft hem niet te onthouden."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,15 @@
|
||||||
"name": "default_password",
|
"name": "default_password",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Standaard admin wachtwoord",
|
"title": "Standaard admin wachtwoord",
|
||||||
"default": "admin",
|
"default": "",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "Wijzig dit na de eerste aanmelding",
|
"description": "Wijzig dit na de eerste aanmelding",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"secret": true,
|
||||||
|
"generate": true,
|
||||||
|
"help": "Hiermee log je zelf in, dus noteer hem voordat je verder gaat. Stond eerder op 'admin' — dat is de standaard van de leverancier en betekent dat iedereen die de poort vindt erin kan."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,12 +61,15 @@
|
||||||
"name": "admin_password",
|
"name": "admin_password",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Admin wachtwoord",
|
"title": "Admin wachtwoord",
|
||||||
"default": "admin",
|
"default": "",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "Wijzig dit na de eerste aanmelding",
|
"description": "Wijzig dit na de eerste aanmelding",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"secret": true,
|
||||||
|
"generate": true,
|
||||||
|
"help": "Hiermee log je zelf in, dus noteer hem voordat je verder gaat. Stond eerder op 'admin' — dat is de standaard van de leverancier en betekent dat iedereen die de poort vindt erin kan."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,17 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "app_url",
|
"name": "app_url",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Basis-URL",
|
"title": "Basis-URL",
|
||||||
"default": "http://localhost:3010",
|
"default": "http://localhost:3011",
|
||||||
"required": true
|
"required": true,
|
||||||
|
"help": "Het adres waarop je HedgeDoc opent. HedgeDoc gebruikt dit voor deel-links en voor de terugkeer na het inloggen, dus een verkeerde poort breekt allebei. Benader je de server op zijn IP-adres, vul dan dat adres in: http://10.0.20.5:3011"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,10 @@
|
||||||
"name": "allowed_hosts",
|
"name": "allowed_hosts",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Toegestane hostnamen",
|
"title": "Toegestane hostnamen",
|
||||||
"default": "localhost:3001",
|
"default": "*",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "Komma-gescheiden; anders weigert Homepage het verzoek"
|
"description": "Komma-gescheiden host:poort, of * voor alles",
|
||||||
|
"help": "Homepage weigert sinds v0.9 elk verzoek waarvan de Host-header er niet bij staat; je krijgt dan 'Host validation failed'.\n\nMet * accepteert Homepage elke hostnaam. Dat is hier de standaard omdat je je server meestal op zijn IP-adres benadert en dat adres vooraf niet bekend is.\n\nWil je het strakker: vul dan zelf in waarmee je Homepage opent, bijvoorbeeld 10.0.20.5:3002,homepage.thuis.nl. De poort hoort erbij en moet gelijk zijn aan de webpoort hierboven."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,13 +63,16 @@
|
||||||
"name": "db_password",
|
"name": "db_password",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Database wachtwoord",
|
"title": "Database wachtwoord",
|
||||||
"default": "immich_db_pass",
|
"default": "",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "Wachtwoord voor de interne PostgreSQL database",
|
"description": "Wachtwoord voor de interne PostgreSQL database",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"secret": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"help": "Wordt automatisch ingevuld met een willekeurige waarde. Alleen de containers onderling gebruiken hem, dus je hoeft hem niet te onthouden."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openai_key",
|
"name": "openai_key",
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,6 @@
|
||||||
"default": 8103,
|
"default": 8103,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "data_dir",
|
|
||||||
"type": "str",
|
|
||||||
"title": "Data directory",
|
|
||||||
"default": "/opt/serverup/appdata",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "timezone",
|
"name": "timezone",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
|
|
@ -67,7 +60,10 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"generate": true,
|
||||||
|
"secret": true,
|
||||||
|
"help": "Hiermee log je straks zelf in, dus noteer hem voordat je verder gaat. De knop maakt een sterk wachtwoord; je mag ook je eigen wachtwoord gebruiken."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "db_password",
|
"name": "db_password",
|
||||||
|
|
@ -79,7 +75,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "admin_email",
|
"name": "admin_email",
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,10 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"generate": true,
|
||||||
|
"secret": true,
|
||||||
|
"help": "Hiermee log je straks zelf in, dus noteer hem voordat je verder gaat. De knop maakt een sterk wachtwoord; je mag ook je eigen wachtwoord gebruiken."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,6 @@
|
||||||
"default": 8105,
|
"default": 8105,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "data_dir",
|
|
||||||
"type": "str",
|
|
||||||
"title": "Data directory",
|
|
||||||
"default": "/opt/serverup/appdata",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "timezone",
|
"name": "timezone",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,6 @@
|
||||||
"title": "Web poort",
|
"title": "Web poort",
|
||||||
"default": 8080,
|
"default": 8080,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data_dir",
|
|
||||||
"type": "str",
|
|
||||||
"title": "Data directory",
|
|
||||||
"default": "/opt/serverup/appdata",
|
|
||||||
"required": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -76,11 +69,14 @@
|
||||||
"name": "db_password",
|
"name": "db_password",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Database wachtwoord",
|
"title": "Database wachtwoord",
|
||||||
"default": "miniflux_db_pass",
|
"default": "",
|
||||||
"required": true,
|
"required": true,
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"secret": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"help": "Wordt automatisch ingevuld met een willekeurige waarde. Alleen de containers onderling gebruiken hem, dus je hoeft hem niet te onthouden."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,9 @@
|
||||||
"name": "base_url",
|
"name": "base_url",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Basis-URL",
|
"title": "Basis-URL",
|
||||||
"default": "http://localhost:8101",
|
"default": "http://localhost:8109",
|
||||||
"required": true
|
"required": true,
|
||||||
|
"help": "Het adres waarop ntfy bereikbaar is. Dit staat in de berichten en in de instructies die ntfy toont, en de telefoon-app gebruikt het om te abonneren. Wijs het naar je server: http://10.0.20.5:8109"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "auth_default",
|
"name": "auth_default",
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,9 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,17 +67,22 @@
|
||||||
"description": "Willekeurige lange string voor sessiebeveiliging",
|
"description": "Willekeurige lange string voor sessiebeveiliging",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "minimaal-50-willekeurige-tekens"
|
"placeholder": "minimaal-50-willekeurige-tekens"
|
||||||
}
|
},
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "db_password",
|
"name": "db_password",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Database wachtwoord",
|
"title": "Database wachtwoord",
|
||||||
"default": "paperless_db_pass",
|
"default": "",
|
||||||
"required": true,
|
"required": true,
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"secret": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"help": "Wordt automatisch ingevuld met een willekeurige waarde. Alleen de containers onderling gebruiken hem, dus je hoeft hem niet te onthouden."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "secret_key",
|
"name": "secret_key",
|
||||||
|
|
@ -73,7 +75,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "base_url",
|
"name": "base_url",
|
||||||
|
|
@ -98,7 +102,10 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"generate": true,
|
||||||
|
"secret": true,
|
||||||
|
"help": "Hiermee log je straks zelf in, dus noteer hem voordat je verder gaat. De knop maakt een sterk wachtwoord; je mag ook je eigen wachtwoord gebruiken."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ services:
|
||||||
container_name: << service_name >>
|
container_name: << service_name >>
|
||||||
ports:
|
ports:
|
||||||
- "<< port >>:9090"
|
- "<< port >>:9090"
|
||||||
|
environment:
|
||||||
|
- TZ=<< timezone >>
|
||||||
command:
|
command:
|
||||||
- --config.file=/etc/prometheus/prometheus.yml
|
- --config.file=/etc/prometheus/prometheus.yml
|
||||||
- --storage.tsdb.retention.time=<< retention >>
|
- --storage.tsdb.retention.time=<< retention >>
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,9 @@
|
||||||
"description": "Willekeurige waarde; gebruik de dobbelsteenknop",
|
"description": "Willekeurige waarde; gebruik de dobbelsteenknop",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,9 @@
|
||||||
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
"description": "Gebruik de dobbelsteenknop voor een willekeurige waarde",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,9 @@
|
||||||
"description": "Willekeurige 32-tekens encryptiesleutel, bijv. base64:xxxx...",
|
"description": "Willekeurige 32-tekens encryptiesleutel, bijv. base64:xxxx...",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "base64:willekeurige-string-van-32-tekens"
|
"placeholder": "base64:willekeurige-string-van-32-tekens"
|
||||||
}
|
},
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "secret_key",
|
"name": "secret_key",
|
||||||
|
|
@ -73,7 +75,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "willekeurige-lange-string"
|
"placeholder": "willekeurige-lange-string"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,6 @@
|
||||||
"default": 4000,
|
"default": 4000,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "data_dir",
|
|
||||||
"type": "str",
|
|
||||||
"title": "Data directory",
|
|
||||||
"default": "/opt/serverup/appdata",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "timezone",
|
"name": "timezone",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
|
|
@ -62,7 +55,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "encryption_key",
|
"name": "encryption_key",
|
||||||
|
|
@ -74,7 +69,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "grafana_port",
|
"name": "grafana_port",
|
||||||
|
|
|
||||||
|
|
@ -109,12 +109,15 @@
|
||||||
"name": "mongo_password",
|
"name": "mongo_password",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "MongoDB wachtwoord",
|
"title": "MongoDB wachtwoord",
|
||||||
"default": "unifi_mongo_pass",
|
"default": "",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "Wachtwoord voor de interne MongoDB database",
|
"description": "Wachtwoord voor de interne MongoDB database",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"secret": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"help": "Wordt automatisch ingevuld met een willekeurige waarde. Alleen de containers onderling gebruiken hem, dus je hoeft hem niet te onthouden."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,9 @@
|
||||||
"description": "Willekeurige geheime string voor het /admin paneel. Laat leeg om admin uit te schakelen.",
|
"description": "Willekeurige geheime string voor het /admin paneel. Laat leeg om admin uit te schakelen.",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "willekeurige-geheime-string"
|
"placeholder": "willekeurige-geheime-string"
|
||||||
}
|
},
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,17 +66,22 @@
|
||||||
"description": "Willekeurige geheime string voor JWT-tokenbeveiliging",
|
"description": "Willekeurige geheime string voor JWT-tokenbeveiliging",
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "willekeurige-lange-string"
|
"placeholder": "willekeurige-lange-string"
|
||||||
}
|
},
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "db_password",
|
"name": "db_password",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"title": "Database wachtwoord",
|
"title": "Database wachtwoord",
|
||||||
"default": "vikunja_db_pass",
|
"default": "",
|
||||||
"required": true,
|
"required": true,
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
}
|
},
|
||||||
|
"secret": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"help": "Wordt automatisch ingevuld met een willekeurige waarde. Alleen de containers onderling gebruiken hem, dus je hoeft hem niet te onthouden."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "app_url",
|
"name": "app_url",
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@
|
||||||
"config": {
|
"config": {
|
||||||
"placeholder": "sterk-wachtwoord"
|
"placeholder": "sterk-wachtwoord"
|
||||||
},
|
},
|
||||||
"advanced": true
|
"advanced": true,
|
||||||
|
"generate": "auto",
|
||||||
|
"secret": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,8 +160,36 @@ Deze namen worden speciaal behandeld:
|
||||||
alles in de stack. Wordt automatisch gevuld met de instantienaam.
|
alles in de stack. Wordt automatisch gevuld met de instantienaam.
|
||||||
- **Velden met `port` in de naam en type `int`** — krijgen bij het installeren
|
- **Velden met `port` in de naam en type `int`** — krijgen bij het installeren
|
||||||
automatisch een vrij poortnummer voorgesteld als de standaard al bezet is.
|
automatisch een vrij poortnummer voorgesteld als de standaard al bezet is.
|
||||||
- **Velden met `token`, `password`, `secret` of `key` in de naam** — krijgen een
|
- **Velden met `token`, `password`, `secret`, `wachtwoord`, `jwt` of `key` in de
|
||||||
knop die een willekeurige waarde genereert.
|
naam** — worden als geheim behandeld: de waarde wordt gemaskeerd in het
|
||||||
|
installatielog en het veld krijgt een eigen weergave. Je kunt het ook
|
||||||
|
afdwingen met `"secret": true`.
|
||||||
|
|
||||||
|
### Waarden laten genereren
|
||||||
|
|
||||||
|
Voor een databasewachtwoord of een JWT-sleutel heeft de gebruiker geen zinnige
|
||||||
|
keuze; die moet gewoon willekeurig zijn. Zet daarvoor `generate`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"name": "db_password", "type": "str", "title": "Databasewachtwoord",
|
||||||
|
"default": "", "required": true, "generate": "auto"}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Waarde | Wat er gebeurt |
|
||||||
|
|---|---|
|
||||||
|
| `"auto"` | Het veld wordt bij het openen van het formulier gevuld met 32 willekeurige tekens. Voor waarden die de gebruiker nooit hoeft te kennen. |
|
||||||
|
| `true` | Alleen een knop naast het veld. Voor wachtwoorden waarmee de gebruiker zélf inlogt: die moet hij noteren, dus vul ze niet ongevraagd in. |
|
||||||
|
| weglaten | Geen knop. Voor waarden met een eigen formaat of herkomst — een bcrypt-hash, een token uit een andere app, een sleutel die bij iets anders moet passen. Iets verzonnens is daar juist fout. |
|
||||||
|
|
||||||
|
> **Zet nooit een wachtwoord als `default`.** Dan krijgt iedereen die de app
|
||||||
|
> installeert hetzelfde, en de meesten klikken erdoorheen. Een test weigert
|
||||||
|
> sjablonen met een vaste waarde op een geheim veld.
|
||||||
|
|
||||||
|
### Verplichte velden
|
||||||
|
|
||||||
|
`"required": true` wordt afgedwongen: een lege waarde levert een foutmelding op
|
||||||
|
in plaats van een container die niet start of met een leeg geheim draait. Zit
|
||||||
|
het veld achter een groepsschakelaar die uitstaat, dan telt het niet mee.
|
||||||
|
|
||||||
### Geavanceerde velden
|
### Geavanceerde velden
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -841,6 +841,13 @@ def api_stack_reconfigure(name):
|
||||||
verbinden = bool(meta.get("connected"))
|
verbinden = bool(meta.get("connected"))
|
||||||
gedeeld_net = (cfg.load().get("SHARED_NETWORK") or "serverup").strip()
|
gedeeld_net = (cfg.load().get("SHARED_NETWORK") or "serverup").strip()
|
||||||
|
|
||||||
|
# Ook hier: een veld leegmaken mag niet stilzwijgend een kapotte stack
|
||||||
|
# opleveren — en er ligt al een werkende versie die we niet moeten slopen.
|
||||||
|
ontbreekt = boilerplates.ontbrekende_verplichte(bron, nieuwe_waarden)
|
||||||
|
if ontbreekt:
|
||||||
|
return jsonify(ok=False, msg="Nog niet ingevuld: " + ", ".join(ontbreekt),
|
||||||
|
missing=ontbreekt), 400
|
||||||
|
|
||||||
jid, q = jobs.create(f"reconfigure:{name}")
|
jid, q = jobs.create(f"reconfigure:{name}")
|
||||||
|
|
||||||
def _run(jid):
|
def _run(jid):
|
||||||
|
|
@ -1348,6 +1355,17 @@ def api_store_install():
|
||||||
if not is_bp and not docker.has_compose(src):
|
if not is_bp and not docker.has_compose(src):
|
||||||
return jsonify(ok=False, msg=f"'{stack}' heeft geen compose bestand")
|
return jsonify(ok=False, msg=f"'{stack}' heeft geen compose bestand")
|
||||||
|
|
||||||
|
# Verplichte velden vóór het aanmaken van de job: een leeg wachtwoord of
|
||||||
|
# een lege sleutel levert een container op die niet start of die met een
|
||||||
|
# leeg geheim draait. Het formulier toonde alleen een sterretje.
|
||||||
|
if is_bp:
|
||||||
|
waarden = dict(bp_values or {})
|
||||||
|
waarden.setdefault("service_name", inst)
|
||||||
|
ontbreekt = boilerplates.ontbrekende_verplichte(src, waarden)
|
||||||
|
if ontbreekt:
|
||||||
|
return jsonify(ok=False, msg="Nog niet ingevuld: " + ", ".join(ontbreekt),
|
||||||
|
missing=ontbreekt), 400
|
||||||
|
|
||||||
lib = Path(cfg.load()["LIBRARY_DIR"]).resolve()
|
lib = Path(cfg.load()["LIBRARY_DIR"]).resolve()
|
||||||
if lib == Path("/app").resolve():
|
if lib == Path("/app").resolve():
|
||||||
return jsonify(ok=False, msg="LIBRARY_DIR mag niet /app zijn.")
|
return jsonify(ok=False, msg="LIBRARY_DIR mag niet /app zijn.")
|
||||||
|
|
@ -1379,8 +1397,11 @@ def api_store_install():
|
||||||
# Defaults voor service_name als de gebruiker niets opgaf
|
# Defaults voor service_name als de gebruiker niets opgaf
|
||||||
values.setdefault("service_name", inst)
|
values.setdefault("service_name", inst)
|
||||||
jobs.log(qq, "info", f"Boilerplate render ({len(values)} waarden)")
|
jobs.log(qq, "info", f"Boilerplate render ({len(values)} waarden)")
|
||||||
|
# Wachtwoorden en sleutels niet uitschrijven: dit log staat in
|
||||||
|
# de interface en wordt vaak gekopieerd in een bugmelding.
|
||||||
|
geheim = {f["name"] for f in boilerplates.fields(src) if f.get("secret")}
|
||||||
for k, v in sorted(values.items()):
|
for k, v in sorted(values.items()):
|
||||||
jobs.log(qq, "dim", f" {k}={v}")
|
jobs.log(qq, "dim", f" {k}=" + ("••••••" if k in geheim and v else str(v)))
|
||||||
try:
|
try:
|
||||||
written = boilerplates.render_to_dir(src, dest, values)
|
written = boilerplates.render_to_dir(src, dest, values)
|
||||||
for rel in written:
|
for rel in written:
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,76 @@ def fields(d: Path) -> list[dict]:
|
||||||
# leeg tekstveld. {"app": "mosquitto", "scheme": "mqtt",
|
# leeg tekstveld. {"app": "mosquitto", "scheme": "mqtt",
|
||||||
# "port": 1883, "path": ""}
|
# "port": 1883, "path": ""}
|
||||||
"connect": item.get("connect") or None,
|
"connect": item.get("connect") or None,
|
||||||
|
# Wachtwoorden en sleutels: het invoerveld verbergt de tekst en
|
||||||
|
# de waarde wordt gemaskeerd in het installatielog.
|
||||||
|
"secret": bool(item.get("secret")) or _lijkt_geheim(item),
|
||||||
|
# Waarden die de gebruiker toch niet zelf kan bedenken (JWT-
|
||||||
|
# sleutels, database-wachtwoorden) worden voorgevuld met iets
|
||||||
|
# willekeurigs. "hex32" = 32 hexadecimale tekens.
|
||||||
|
"generate": item.get("generate") or "",
|
||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# Namen die vrijwel altijd een geheim aanduiden. Bedoeld om te voorkomen dat een
|
||||||
|
# nieuw template het per ongeluk als gewoon tekstveld toont.
|
||||||
|
_GEHEIM_WOORDEN = ("password", "passwd", "secret", "token", "apikey",
|
||||||
|
"jwt", "wachtwoord", "sleutel", "adminpass")
|
||||||
|
# Losse woorddelen: 'key' als heel woord in de naam (app_key, agent_key,
|
||||||
|
# wireguard_private_key), maar niet in iets als 'keyboard_layout'.
|
||||||
|
_GEHEIM_DELEN = ("key", "keys")
|
||||||
|
|
||||||
|
|
||||||
|
def _lijkt_geheim(item: dict) -> bool:
|
||||||
|
naam = (item.get("name") or "").lower()
|
||||||
|
if any(w in naam for w in _GEHEIM_WOORDEN):
|
||||||
|
return True
|
||||||
|
return any(deel in _GEHEIM_DELEN for deel in naam.split("_"))
|
||||||
|
|
||||||
|
|
||||||
|
def ontbrekende_verplichte(d: Path, values: dict[str, Any]) -> list[str]:
|
||||||
|
"""Verplichte velden zonder waarde.
|
||||||
|
|
||||||
|
Zonder deze controle installeerde je een app met een leeg wachtwoord of een
|
||||||
|
lege sleutel: de container start dan met een onbruikbare configuratie, of —
|
||||||
|
erger — met een geheim dat leeg is. Het formulier toonde alleen een
|
||||||
|
sterretje en hield niets tegen.
|
||||||
|
"""
|
||||||
|
ontbreekt = []
|
||||||
|
for veld in fields(d):
|
||||||
|
if not veld.get("required") or veld.get("is_group_toggle"):
|
||||||
|
continue
|
||||||
|
# Velden die achter een uitgeschakelde groepsschakelaar zitten tellen
|
||||||
|
# niet mee; die komen sowieso niet in het compose-bestand terecht.
|
||||||
|
toggle = veld.get("group_toggle")
|
||||||
|
if toggle and not _waarheid(values.get(toggle, True)):
|
||||||
|
continue
|
||||||
|
if not _waarheid(veld.get("needs"), values):
|
||||||
|
continue
|
||||||
|
waarde = values.get(veld["name"], veld.get("default"))
|
||||||
|
if waarde is None or (isinstance(waarde, str) and not waarde.strip()):
|
||||||
|
ontbreekt.append(veld.get("title") or veld["name"])
|
||||||
|
return ontbreekt
|
||||||
|
|
||||||
|
|
||||||
|
def _waarheid(waarde, values: dict | None = None) -> bool:
|
||||||
|
"""Evalueer een schakelaarwaarde, of een `needs`-lijst tegen de waarden."""
|
||||||
|
if values is not None:
|
||||||
|
# waarde is hier de needs-lijst: elke voorwaarde moet kloppen.
|
||||||
|
for nodig in (waarde or []):
|
||||||
|
if isinstance(nodig, str):
|
||||||
|
if not _waarheid(values.get(nodig)):
|
||||||
|
return False
|
||||||
|
elif isinstance(nodig, dict):
|
||||||
|
naam = nodig.get("name") or nodig.get("field")
|
||||||
|
if naam and str(values.get(naam, "")) != str(nodig.get("value", "")):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
if isinstance(waarde, str):
|
||||||
|
return waarde.strip().lower() not in ("", "0", "false", "no", "nee", "off")
|
||||||
|
return bool(waarde)
|
||||||
|
|
||||||
|
|
||||||
# ── Templating engine ────────────────────────────────────────────────────────
|
# ── Templating engine ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class BoilerplateError(Exception):
|
class BoilerplateError(Exception):
|
||||||
|
|
|
||||||
|
|
@ -1482,7 +1482,7 @@ tailwind.config = {
|
||||||
<template x-if="!f.connect && f.type === 'str' && f.secret">
|
<template x-if="!f.connect && f.type === 'str' && f.secret">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input class="form-input font-mono !text-xs" x-model="install.values[f.name]" :placeholder="f.placeholder">
|
<input class="form-input font-mono !text-xs" x-model="install.values[f.name]" :placeholder="f.placeholder">
|
||||||
<button type="button" class="btn-sm shrink-0" :title="t('generate_value')"
|
<button type="button" class="btn-sm shrink-0" x-show="f.generate" :title="t('generate_value')"
|
||||||
@click="install.values[f.name] = randomSecret()"><span class="mdi mdi-dice-5-outline"></span></button>
|
@click="install.values[f.name] = randomSecret()"><span class="mdi mdi-dice-5-outline"></span></button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1543,7 +1543,7 @@ tailwind.config = {
|
||||||
<template x-if="!f.connect && f.type === 'str' && f.secret">
|
<template x-if="!f.connect && f.type === 'str' && f.secret">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input class="form-input font-mono !text-xs" x-model="install.values[f.name]" :placeholder="f.placeholder">
|
<input class="form-input font-mono !text-xs" x-model="install.values[f.name]" :placeholder="f.placeholder">
|
||||||
<button type="button" class="btn-sm shrink-0" :title="t('generate_value')"
|
<button type="button" class="btn-sm shrink-0" x-show="f.generate" :title="t('generate_value')"
|
||||||
@click="install.values[f.name] = randomSecret()"><span class="mdi mdi-dice-5-outline"></span></button>
|
@click="install.values[f.name] = randomSecret()"><span class="mdi mdi-dice-5-outline"></span></button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2541,6 +2541,14 @@ function app() {
|
||||||
const doelen = this.connectTargets(f);
|
const doelen = this.connectTargets(f);
|
||||||
if (doelen.length) init[f.name] = doelen[0].value;
|
if (doelen.length) init[f.name] = doelen[0].value;
|
||||||
}
|
}
|
||||||
|
// Database-wachtwoorden en interne sleutels meteen invullen. Die hoef
|
||||||
|
// je nooit zelf te typen, en een leeg veld leverde een stack op die
|
||||||
|
// niet startte of met een leeg geheim draaide.
|
||||||
|
for (const f of (d.fields || [])) {
|
||||||
|
// 'auto': waarden die je nooit hoeft te kennen. Een inlogwachtwoord
|
||||||
|
// krijgt alleen een knop, want dat moet je zelf noteren.
|
||||||
|
if (f.generate === 'auto' && !init[f.name]) init[f.name] = this.randomSecret();
|
||||||
|
}
|
||||||
this.install.values = init;
|
this.install.values = init;
|
||||||
this.install.preview_files = d.preview_files || {};
|
this.install.preview_files = d.preview_files || {};
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
222
tests/test_app_templates.py
Normal file
222
tests/test_app_templates.py
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
"""Alle app-sjablonen doorlopen op fouten die pas bij het installeren opvallen.
|
||||||
|
|
||||||
|
Deze controles zijn ontstaan uit echte fouten: een Homepage die met
|
||||||
|
"Host validation failed" weigerde omdat de standaardwaarde een andere poort
|
||||||
|
noemde dan het poortveld, en velden die in het formulier stonden maar nergens
|
||||||
|
in het compose-bestand terechtkwamen.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
WORTEL = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(WORTEL / "server-up"))
|
||||||
|
os.environ.setdefault("SU_CONFIG", "/tmp/su-templates-test.json")
|
||||||
|
|
||||||
|
from core import boilerplates # noqa: E402
|
||||||
|
|
||||||
|
APPS = WORTEL / "apps"
|
||||||
|
VAR_RE = re.compile(r"<<\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\|[^>]*?)?>>")
|
||||||
|
BLOK_RE = re.compile(r"<%-?\s*(?:if|elif)\s+([^%]+?)\s*-?%>")
|
||||||
|
SLEUTELWOORDEN = {"and", "or", "not", "in", "is", "if", "elif",
|
||||||
|
"true", "false", "none", "defined"}
|
||||||
|
|
||||||
|
|
||||||
|
def _apps():
|
||||||
|
return sorted(d for d in APPS.iterdir()
|
||||||
|
if d.is_dir() and (d / "template.json").is_file())
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_bestanden(d):
|
||||||
|
f = d / "files"
|
||||||
|
return sorted(p for p in f.iterdir()
|
||||||
|
if f.is_dir() and p.suffix in (".yaml", ".yml")
|
||||||
|
and "compose" in p.name.lower())
|
||||||
|
|
||||||
|
|
||||||
|
def _gerenderd(d):
|
||||||
|
"""Render elk compose-bestand met de standaardwaarden en geef de YAML."""
|
||||||
|
velden = boilerplates.fields(d)
|
||||||
|
waarden = {f["name"]: (f["default"] if f["default"] is not None else "")
|
||||||
|
for f in velden}
|
||||||
|
uit = []
|
||||||
|
for p in _compose_bestanden(d):
|
||||||
|
tekst = boilerplates.render_text(p.read_text(encoding="utf-8"), waarden)
|
||||||
|
uit.append((p, yaml.safe_load(tekst)))
|
||||||
|
return uit
|
||||||
|
|
||||||
|
|
||||||
|
IDS = [d.name for d in _apps()]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_template_is_geldige_json(app):
|
||||||
|
json.loads((app / "template.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_rendert_naar_geldige_compose(app):
|
||||||
|
for pad, doc in _gerenderd(app):
|
||||||
|
assert isinstance(doc, dict), f"{pad.name} levert geen mapping op"
|
||||||
|
assert doc.get("services"), f"{pad.name} heeft geen services"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_alle_variabelen_zijn_gedefinieerd(app):
|
||||||
|
"""Een variabele zonder veld rendert leeg; dat levert stilzwijgend een
|
||||||
|
kapotte instelling op in plaats van een foutmelding."""
|
||||||
|
namen = {f["name"] for f in boilerplates.fields(app)}
|
||||||
|
for p in _compose_bestanden(app):
|
||||||
|
ruw = p.read_text(encoding="utf-8")
|
||||||
|
gebruikt = set(VAR_RE.findall(ruw))
|
||||||
|
for blok in BLOK_RE.findall(ruw):
|
||||||
|
gebruikt |= (set(re.findall(r"[a-zA-Z_][a-zA-Z0-9_]*", blok))
|
||||||
|
- SLEUTELWOORDEN)
|
||||||
|
onbekend = sorted(gebruikt - namen)
|
||||||
|
assert not onbekend, f"{p.name}: niet gedefinieerd: {onbekend}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_geen_velden_die_nergens_gebruikt_worden(app):
|
||||||
|
"""Een veld dat nergens terechtkomt, liegt tegen de gebruiker: je vult een
|
||||||
|
datamap in en je gegevens staan ergens anders."""
|
||||||
|
velden = [f for f in boilerplates.fields(app) if not f.get("is_group_toggle")]
|
||||||
|
gebruikt = set()
|
||||||
|
for p in _compose_bestanden(app):
|
||||||
|
ruw = p.read_text(encoding="utf-8")
|
||||||
|
gebruikt |= set(VAR_RE.findall(ruw))
|
||||||
|
for blok in BLOK_RE.findall(ruw):
|
||||||
|
gebruikt |= set(re.findall(r"[a-zA-Z_][a-zA-Z0-9_]*", blok))
|
||||||
|
# Ook andere bestanden in files/ mogen variabelen gebruiken.
|
||||||
|
for p in (app / "files").rglob("*"):
|
||||||
|
if p.is_file() and p.suffix not in (".yaml", ".yml"):
|
||||||
|
try:
|
||||||
|
gebruikt |= set(VAR_RE.findall(p.read_text(encoding="utf-8")))
|
||||||
|
except (UnicodeDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
ongebruikt = sorted(f["name"] for f in velden if f["name"] not in gebruikt)
|
||||||
|
assert not ongebruikt, f"velden zonder werking: {ongebruikt}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_named_volumes_staan_bovenaan(app):
|
||||||
|
"""Compose weigert een volume dat niet in het top-level blok staat."""
|
||||||
|
for pad, doc in _gerenderd(app):
|
||||||
|
top = set(doc.get("volumes") or {})
|
||||||
|
for snaam, svc in (doc.get("services") or {}).items():
|
||||||
|
for v in (svc or {}).get("volumes") or []:
|
||||||
|
if not isinstance(v, str):
|
||||||
|
continue
|
||||||
|
bron = v.split(":")[0]
|
||||||
|
if bron and not bron.startswith(("/", ".", "~", "$")):
|
||||||
|
assert bron in top, f"{pad.name}: {snaam} gebruikt '{bron}'"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_depends_on_verwijst_naar_bestaande_services(app):
|
||||||
|
for pad, doc in _gerenderd(app):
|
||||||
|
services = doc.get("services") or {}
|
||||||
|
for snaam, svc in services.items():
|
||||||
|
dep = (svc or {}).get("depends_on")
|
||||||
|
for d in (list(dep) if isinstance(dep, (list, dict)) else []):
|
||||||
|
assert d in services, f"{pad.name}: {snaam} → onbekend '{d}'"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_geen_dubbele_containernamen(app):
|
||||||
|
for pad, doc in _gerenderd(app):
|
||||||
|
namen = [svc.get("container_name") for svc in (doc.get("services") or {}).values()
|
||||||
|
if isinstance(svc, dict) and svc.get("container_name")]
|
||||||
|
assert len(namen) == len(set(namen)), f"{pad.name}: dubbel in {namen}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_geen_poortconflict_binnen_een_stack(app):
|
||||||
|
"""Twee services die dezelfde hostpoort claimen levert een stack op die
|
||||||
|
half start. TCP en UDP op hetzelfde nummer mag wel."""
|
||||||
|
for pad, doc in _gerenderd(app):
|
||||||
|
bezet = {}
|
||||||
|
for snaam, svc in (doc.get("services") or {}).items():
|
||||||
|
for pr in (svc or {}).get("ports") or []:
|
||||||
|
s = str(pr)
|
||||||
|
proto = "udp" if s.endswith("/udp") else "tcp"
|
||||||
|
deel = s.replace("/udp", "").replace("/tcp", "").split(":")
|
||||||
|
if len(deel) >= 2 and deel[-2].strip('"').isdigit():
|
||||||
|
bezet.setdefault((deel[-2].strip('"'), proto), []).append(snaam)
|
||||||
|
dubbel = {k: v for k, v in bezet.items() if len(v) > 1}
|
||||||
|
assert not dubbel, f"{pad.name}: {dubbel}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_poort_in_een_standaardwaarde_klopt_met_het_poortveld(app):
|
||||||
|
"""Homepage weigerde met 'Host validation failed' omdat allowed_hosts
|
||||||
|
poort 3001 noemde terwijl het poortveld op 3002 stond. Dezelfde fout zat in
|
||||||
|
baserow, hedgedoc en ntfy."""
|
||||||
|
velden = boilerplates.fields(app)
|
||||||
|
poorten = {f["default"] for f in velden
|
||||||
|
if f["type"] == "int" and "port" in f["name"]
|
||||||
|
and isinstance(f["default"], int)}
|
||||||
|
if not poorten:
|
||||||
|
return
|
||||||
|
for f in velden:
|
||||||
|
d = f.get("default")
|
||||||
|
if not isinstance(d, str):
|
||||||
|
continue
|
||||||
|
for m in re.finditer(r"[a-zA-Z0-9_.-]+:(\d{2,5})\b", d):
|
||||||
|
genoemd = int(m.group(1))
|
||||||
|
# Alleen poorten die duidelijk bij deze app horen; een willekeurig
|
||||||
|
# ander nummer (een SMTP-poort bijvoorbeeld) valt hier buiten.
|
||||||
|
if any(abs(genoemd - p) < 1000 for p in poorten):
|
||||||
|
assert genoemd in poorten, (
|
||||||
|
f"{f['name']}={d!r} noemt poort {genoemd}, "
|
||||||
|
f"maar de poortvelden staan op {sorted(poorten)}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_wachtwoordvelden_zijn_als_geheim_gemarkeerd(app):
|
||||||
|
"""Anders staat het wachtwoord leesbaar in beeld en ontbreekt de knop om er
|
||||||
|
een te laten maken."""
|
||||||
|
for f in boilerplates.fields(app):
|
||||||
|
naam = f["name"].lower()
|
||||||
|
if any(w in naam for w in ("password", "secret", "token", "_key")):
|
||||||
|
assert f["secret"] is True, f"{f['name']} is niet als geheim gemarkeerd"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_geen_vast_wachtwoord_in_het_sjabloon(app):
|
||||||
|
"""Acht apps hadden er een: 'immich_db_pass' voor iedereen die het
|
||||||
|
installeert, en 'admin' als beheerderswachtwoord van Grafana en Gotify.
|
||||||
|
Een standaardwaarde die overal hetzelfde is, is geen wachtwoord."""
|
||||||
|
for f in boilerplates.fields(app):
|
||||||
|
if not f.get("secret"):
|
||||||
|
continue
|
||||||
|
waarde = str(f.get("default") or "").strip()
|
||||||
|
assert not waarde, (
|
||||||
|
f"{f['name']} heeft een vaste waarde ({waarde!r}); maak het veld "
|
||||||
|
f"leeg en zet 'generate' aan, of laat de gebruiker het invullen")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app", _apps(), ids=IDS)
|
||||||
|
def test_verplichte_geheimen_zijn_invulbaar_of_worden_gegenereerd(app):
|
||||||
|
"""Een verplicht geheim zonder standaardwaarde moet óf automatisch gevuld
|
||||||
|
worden, óf iets zijn dat de gebruiker zelf aanlevert. Wat overblijft is een
|
||||||
|
veld waar niemand een geldige waarde voor kan verzinnen."""
|
||||||
|
# Wachtwoorden waarmee je zelf inlogt en tokens die je elders ophaalt vult
|
||||||
|
# de gebruiker bewust zelf in.
|
||||||
|
eigen_invoer = ("admin_password", "web_password", "ui_password",
|
||||||
|
"superuser_password", "password_hash", "hass_token",
|
||||||
|
"forgejo_token", "adminpass")
|
||||||
|
for f in boilerplates.fields(app):
|
||||||
|
if not (f.get("required") and f.get("secret")):
|
||||||
|
continue
|
||||||
|
if str(f.get("default") or "").strip():
|
||||||
|
continue
|
||||||
|
if f.get("generate") or f["name"] in eigen_invoer:
|
||||||
|
continue
|
||||||
|
pytest.fail(f"{f['name']}: verplicht geheim zonder waarde, zonder "
|
||||||
|
f"generator en niet als eigen invoer aangemerkt")
|
||||||
160
tests/test_verplichte_velden.py
Normal file
160
tests/test_verplichte_velden.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
"""Verplichte velden mogen niet leeg blijven, en geheimen horen gemaskeerd.
|
||||||
|
|
||||||
|
Het formulier zette een sterretje achter een verplicht veld en hield verder
|
||||||
|
niets tegen: je kon een app installeren met een leeg databasewachtwoord of een
|
||||||
|
lege JWT-sleutel. De container start dan niet, of draait met een leeg geheim.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up"))
|
||||||
|
|
||||||
|
from conftest import login
|
||||||
|
|
||||||
|
|
||||||
|
def _maak_bron(env, naam="testapp", items=None, groepen=None):
|
||||||
|
"""Zet een app-sjabloon neer waar _find_stack_src het verwacht."""
|
||||||
|
from core import git
|
||||||
|
wortel = git.cache_dir("server-up") / "apps" / naam
|
||||||
|
(wortel / "files").mkdir(parents=True, exist_ok=True)
|
||||||
|
variabelen = groepen or [{"title": "Algemeen", "items": items or []}]
|
||||||
|
(wortel / "template.json").write_text(json.dumps({
|
||||||
|
"kind": "compose",
|
||||||
|
"metadata": {"name": naam, "description": "test"},
|
||||||
|
"variables": variabelen,
|
||||||
|
}), encoding="utf-8")
|
||||||
|
(wortel / "files" / "compose.yaml").write_text(
|
||||||
|
"services:\n"
|
||||||
|
" << service_name >>:\n"
|
||||||
|
" image: nginx\n"
|
||||||
|
" environment:\n"
|
||||||
|
" - DB_PASSWORD=<< db_password >>\n", encoding="utf-8")
|
||||||
|
return wortel
|
||||||
|
|
||||||
|
|
||||||
|
VELDEN = [
|
||||||
|
{"name": "service_name", "type": "str", "title": "Naam",
|
||||||
|
"default": "testapp", "required": True},
|
||||||
|
{"name": "db_password", "type": "str", "title": "Databasewachtwoord",
|
||||||
|
"default": "", "required": True},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_leeg_verplicht_veld_wordt_geweigerd(client, env):
|
||||||
|
csrf = login(client)
|
||||||
|
_maak_bron(env, items=VELDEN)
|
||||||
|
r = client.post("/api/store/install",
|
||||||
|
json={"stack": "testapp", "repo_id": "server-up",
|
||||||
|
"instance": "t1", "values": {"db_password": ""}},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 400, r.get_json()
|
||||||
|
d = r.get_json()
|
||||||
|
assert "Databasewachtwoord" in d["msg"]
|
||||||
|
assert "Databasewachtwoord" in d["missing"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ontbrekend_veld_wordt_geweigerd(client, env):
|
||||||
|
"""Niet meegestuurd is net zo fout als leeg meegestuurd."""
|
||||||
|
csrf = login(client)
|
||||||
|
_maak_bron(env, items=VELDEN)
|
||||||
|
r = client.post("/api/store/install",
|
||||||
|
json={"stack": "testapp", "repo_id": "server-up",
|
||||||
|
"instance": "t2", "values": {}},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert "Databasewachtwoord" in r.get_json()["missing"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_alleen_spaties_telt_als_leeg(client, env):
|
||||||
|
csrf = login(client)
|
||||||
|
_maak_bron(env, items=VELDEN)
|
||||||
|
r = client.post("/api/store/install",
|
||||||
|
json={"stack": "testapp", "repo_id": "server-up",
|
||||||
|
"instance": "t3", "values": {"db_password": " "}},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_ingevuld_veld_gaat_door(client, env):
|
||||||
|
csrf = login(client)
|
||||||
|
_maak_bron(env, items=VELDEN)
|
||||||
|
r = client.post("/api/store/install",
|
||||||
|
json={"stack": "testapp", "repo_id": "server-up",
|
||||||
|
"instance": "t4", "values": {"db_password": "geheim123"}},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 200, r.get_json()
|
||||||
|
assert r.get_json().get("job_id")
|
||||||
|
|
||||||
|
|
||||||
|
def test_veld_met_een_default_hoeft_niet_meegestuurd(client, env):
|
||||||
|
"""service_name heeft een standaardwaarde; die telt als ingevuld."""
|
||||||
|
csrf = login(client)
|
||||||
|
_maak_bron(env, items=VELDEN)
|
||||||
|
r = client.post("/api/store/install",
|
||||||
|
json={"stack": "testapp", "repo_id": "server-up",
|
||||||
|
"instance": "t5", "values": {"db_password": "x"}},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 200, r.get_json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_veld_in_uitgeschakelde_groep_telt_niet_mee(client, env):
|
||||||
|
"""Staat de groepsschakelaar uit, dan komt het veld niet in compose terecht
|
||||||
|
en mag het dus leeg blijven."""
|
||||||
|
csrf = login(client)
|
||||||
|
_maak_bron(env, groepen=[
|
||||||
|
{"title": "Algemeen", "items": [VELDEN[0]]},
|
||||||
|
{"title": "Database", "toggle": "gebruik_db", "toggle_default": False,
|
||||||
|
"items": [{"name": "db_password", "type": "str", "title": "Wachtwoord",
|
||||||
|
"default": "", "required": True}]},
|
||||||
|
])
|
||||||
|
r = client.post("/api/store/install",
|
||||||
|
json={"stack": "testapp", "repo_id": "server-up",
|
||||||
|
"instance": "t6",
|
||||||
|
"values": {"gebruik_db": False, "db_password": ""}},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 200, r.get_json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_veld_in_ingeschakelde_groep_telt_wel_mee(client, env):
|
||||||
|
csrf = login(client)
|
||||||
|
_maak_bron(env, groepen=[
|
||||||
|
{"title": "Algemeen", "items": [VELDEN[0]]},
|
||||||
|
{"title": "Database", "toggle": "gebruik_db", "toggle_default": True,
|
||||||
|
"items": [{"name": "db_password", "type": "str", "title": "Wachtwoord",
|
||||||
|
"default": "", "required": True}]},
|
||||||
|
])
|
||||||
|
r = client.post("/api/store/install",
|
||||||
|
json={"stack": "testapp", "repo_id": "server-up",
|
||||||
|
"instance": "t7",
|
||||||
|
"values": {"gebruik_db": True, "db_password": ""}},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert "Wachtwoord" in r.get_json()["missing"]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Geheimen herkennen ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("naam", [
|
||||||
|
"db_password", "admin_password", "jwt_secret", "secret_key", "api_key",
|
||||||
|
"app_key", "encryption_key", "admin_token", "hass_token",
|
||||||
|
])
|
||||||
|
def test_geheime_velden_worden_herkend(env, naam):
|
||||||
|
"""Zonder deze markering toonde de interface het wachtwoord als gewone
|
||||||
|
tekst én verscheen de dobbelsteenknop niet, terwijl de beschrijving van
|
||||||
|
zeventien apps ernaar verwijst."""
|
||||||
|
from core import boilerplates
|
||||||
|
wortel = _maak_bron(env, items=[
|
||||||
|
{"name": naam, "type": "str", "title": "X", "default": "", "required": True}])
|
||||||
|
veld = boilerplates.fields(wortel)[0]
|
||||||
|
assert veld["secret"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("naam", ["service_name", "port", "data_dir", "timezone"])
|
||||||
|
def test_gewone_velden_zijn_geen_geheim(env, naam):
|
||||||
|
from core import boilerplates
|
||||||
|
wortel = _maak_bron(env, items=[
|
||||||
|
{"name": naam, "type": "str", "title": "X", "default": "x"}])
|
||||||
|
assert boilerplates.fields(wortel)[0]["secret"] is False
|
||||||
Loading…
Reference in a new issue