monitoring: lock Finance (Personal) folder to admin + fix cash classification
Folder ACL:
- Move uk-payslip + wealth dashboards to a new "Finance (Personal)"
folder; job-hunter + fire-planner stay in "Finance" (open).
- New null_resource calls Grafana's folder permissions API after the
dashboard sidecar materialises the folder, setting an admin-only
ACL ({Admin: 4}). Default Viewer/Editor inheritance is overridden,
so anonymous-Viewer (auth.anonymous=true) is denied. Server-admin
always retains access.
- Verified: anonymous → 403 on uk-payslip + wealth, 200 on
control dashboards (node-exporter); admin → 200 on all.
Wealth cash fix:
- Wealthfolio dumps WORKPLACE_PENSION wrappers entirely into
cash_balance because it doesn't track underlying fund holdings.
Reclassify pension cash as invested in the "Cash vs invested"
panel so the cash series reflects actual uninvested broker cash
(~£16k T212 ISA + Schwab) instead of phantom £154k.
Pre-fix: cash=£153,789 / invested=£870,282 / total=£1,024,071
Post-fix: cash=£16,064 / invested=£1,008,008 / total=£1,024,071
This commit is contained in:
parent
51bf38815c
commit
d48e222054
4 changed files with 71 additions and 4 deletions
|
|
@ -30,6 +30,7 @@ module "monitoring" {
|
|||
haos_api_token = data.vault_kv_secret_v2.secrets.data["haos_api_token"]
|
||||
pve_password = data.vault_kv_secret_v2.secrets.data["pve_password"]
|
||||
grafana_admin_password = data.vault_kv_secret_v2.secrets.data["grafana_admin_password"]
|
||||
kube_config_path = var.kube_config_path
|
||||
registry_user = data.vault_kv_secret_v2.viktor.data["registry_user"]
|
||||
registry_password = data.vault_kv_secret_v2.viktor.data["registry_password"]
|
||||
tier = local.tiers.cluster
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@
|
|||
{
|
||||
"id": 9,
|
||||
"title": "Cash vs invested (stacked)",
|
||||
"description": "Daily breakdown of cash holdings vs market value of investments, summed across all accounts.",
|
||||
"description": "Daily breakdown of uninvested broker cash vs market value of investments. WORKPLACE_PENSION accounts (Fidelity) are reclassified entirely as invested — Wealthfolio dumps pension wrappers into cash_balance because it doesn't track the underlying fund holdings, but they are not actually cash.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
|
||||
"gridPos": {"h": 10, "w": 24, "x": 0, "y": 35},
|
||||
|
|
@ -400,7 +400,7 @@
|
|||
"rawQuery": true,
|
||||
"editorMode": "code",
|
||||
"format": "time_series",
|
||||
"rawSql": "SELECT valuation_date::timestamp AS \"time\", SUM(cash_balance) AS cash, SUM(investment_market_value) AS invested FROM daily_account_valuation WHERE $__timeFilter(valuation_date) GROUP BY valuation_date ORDER BY valuation_date"
|
||||
"rawSql": "SELECT d.valuation_date::timestamp AS \"time\", SUM(CASE WHEN a.account_type = 'WORKPLACE_PENSION' THEN 0 ELSE d.cash_balance END) AS cash, SUM(CASE WHEN a.account_type = 'WORKPLACE_PENSION' THEN d.cash_balance + d.investment_market_value ELSE d.investment_market_value END) AS invested FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id WHERE $__timeFilter(d.valuation_date) GROUP BY d.valuation_date ORDER BY d.valuation_date"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -134,11 +134,19 @@ locals {
|
|||
# Applications
|
||||
"qbittorrent.json" = "Applications"
|
||||
"realestate-crawler.json" = "Applications"
|
||||
"uk-payslip.json" = "Finance"
|
||||
"uk-payslip.json" = "Finance (Personal)"
|
||||
"wealth.json" = "Finance (Personal)"
|
||||
"job-hunter.json" = "Finance"
|
||||
"wealth.json" = "Finance"
|
||||
"fire-planner.json" = "Finance"
|
||||
}
|
||||
|
||||
# Folders restricted to the Grafana admin user (anonymous Viewer + any future
|
||||
# non-admin users are denied). Permission set by null_resource below via the
|
||||
# Grafana folder permissions API after the dashboard sidecar auto-creates the
|
||||
# folder. Server-admin always retains access regardless of folder ACL.
|
||||
admin_only_folders = [
|
||||
"Finance (Personal)",
|
||||
]
|
||||
}
|
||||
|
||||
resource "kubernetes_config_map" "grafana_dashboards" {
|
||||
|
|
@ -159,6 +167,60 @@ resource "kubernetes_config_map" "grafana_dashboards" {
|
|||
}
|
||||
}
|
||||
|
||||
# Lock down "admin only" folders via Grafana folder permissions API.
|
||||
# Default org-role inheritance gives Viewer + Editor read access to every
|
||||
# folder; explicitly setting the folder ACL to {Admin: 4} overrides that
|
||||
# inheritance so Viewer/Editor (incl. anonymous-Viewer) get no access.
|
||||
# The Grafana super-admin (`admin` user) always retains access regardless.
|
||||
resource "null_resource" "grafana_admin_only_folder_acl" {
|
||||
for_each = toset(local.admin_only_folders)
|
||||
|
||||
# Re-runs on tg apply (cheap, idempotent API call). Catches drift if anyone
|
||||
# edits permissions via the UI or the folder is rebuilt.
|
||||
triggers = {
|
||||
folder = each.value
|
||||
always = timestamp()
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
interpreter = ["/bin/bash", "-c"]
|
||||
command = <<-EOT
|
||||
set -euo pipefail
|
||||
FOLDER='${each.value}'
|
||||
KUBECONFIG_FLAG='--kubeconfig ${var.kube_config_path}'
|
||||
POD=$(kubectl $KUBECONFIG_FLAG get pod -n monitoring -l app.kubernetes.io/name=grafana -o jsonpath='{.items[0].metadata.name}')
|
||||
ADMIN_PW=$(kubectl $KUBECONFIG_FLAG get secret -n monitoring grafana -o jsonpath='{.data.admin-password}' | base64 -d)
|
||||
|
||||
# Wait up to 60s for the dashboard sidecar to materialise the folder.
|
||||
for i in $(seq 1 12); do
|
||||
FOLDER_UID=$(kubectl $KUBECONFIG_FLAG exec -n monitoring "$POD" -c grafana -- \
|
||||
curl -sf -u "admin:$ADMIN_PW" "http://localhost:3000/api/folders" \
|
||||
| python3 -c "import json,sys; folders=json.load(sys.stdin); print(next((f['uid'] for f in folders if f['title']==sys.argv[1]), ''))" "$FOLDER" || true)
|
||||
if [ -n "$FOLDER_UID" ]; then break; fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ -z "$FOLDER_UID" ]; then
|
||||
echo "ERROR: folder '$FOLDER' not found in Grafana after 60s"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Admin-only ACL. permission codes: 1=View, 2=Edit, 4=Admin.
|
||||
kubectl $KUBECONFIG_FLAG exec -n monitoring "$POD" -c grafana -- \
|
||||
curl -sf -u "admin:$ADMIN_PW" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"items":[{"role":"Admin","permission":4}]}' \
|
||||
"http://localhost:3000/api/folders/$FOLDER_UID/permissions" >/dev/null
|
||||
echo "set admin-only ACL on folder '$FOLDER' (uid=$FOLDER_UID)"
|
||||
EOT
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
helm_release.grafana,
|
||||
kubernetes_config_map.grafana_dashboards,
|
||||
]
|
||||
}
|
||||
|
||||
resource "helm_release" "grafana" {
|
||||
namespace = kubernetes_namespace.monitoring.metadata[0].name
|
||||
create_namespace = true
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ variable "grafana_admin_password" {
|
|||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
variable "kube_config_path" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
variable "tier" { type = string }
|
||||
variable "mysql_host" { type = string }
|
||||
variable "registry_user" {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue