From 594683dbf8ba1374c9c738ce962c629ac115b60a Mon Sep 17 00:00:00 2001 From: cclohmar Date: Wed, 8 Jul 2026 13:46:02 +0100 Subject: [PATCH] feat: RBAC with Authelia groups + filtered launcher + admin panel --- config/authelia/configuration.yml | 70 +++++++ config/authelia/users_database.yml | 8 + deploy.sh | 6 + main.go | 295 ++++++++++++++++++++--------- tools/manage-users.sh | 42 ++++ 5 files changed, 329 insertions(+), 92 deletions(-) create mode 100644 tools/manage-users.sh diff --git a/config/authelia/configuration.yml b/config/authelia/configuration.yml index b11fd4f..e4ae3f2 100644 --- a/config/authelia/configuration.yml +++ b/config/authelia/configuration.yml @@ -21,8 +21,78 @@ totp: access_control: default_policy: deny rules: + # Auth and public pages — no auth required - domain: "auth.{DOMAIN}" policy: bypass + - domain: "www.{DOMAIN}" + policy: bypass + + # Admin panel — admins only + - domain: "app.{DOMAIN}" + resources: + - "^/config(/.*)?$" + subject: + - "group:admins" + policy: one_factor + + # App paths — group-restricted + - domain: "app.{DOMAIN}" + resources: + - "^/drive(/.*)?$" + subject: + - "group:admins" + - "group:drive" + policy: one_factor + + - domain: "app.{DOMAIN}" + resources: + - "^/office(/.*)?$" + subject: + - "group:admins" + - "group:office" + policy: one_factor + + - domain: "app.{DOMAIN}" + resources: + - "^/enterprise(/.*)?$" + subject: + - "group:admins" + - "group:erp" + policy: one_factor + + - domain: "app.{DOMAIN}" + resources: + - "^/chat(/.*)?$" + subject: + - "group:admins" + - "group:chat" + policy: one_factor + + - domain: "app.{DOMAIN}" + resources: + - "^/meet(/.*)?$" + subject: + - "group:admins" + - "group:meet" + policy: one_factor + + - domain: "app.{DOMAIN}" + resources: + - "^/connect(/.*)?$" + subject: + - "group:admins" + - "group:mail" + policy: one_factor + + - domain: "app.{DOMAIN}" + resources: + - "^/aida(/.*)?$" + subject: + - "group:admins" + - "group:ai" + policy: one_factor + + # Home/launcher — any authenticated user - domain: "app.{DOMAIN}" policy: one_factor diff --git a/config/authelia/users_database.yml b/config/authelia/users_database.yml index 65e7012..9f91205 100644 --- a/config/authelia/users_database.yml +++ b/config/authelia/users_database.yml @@ -6,3 +6,11 @@ users: email: "{TLS_EMAIL}" groups: - admins + - users + - drive + - office + - erp + - chat + - meet + - mail + - ai diff --git a/deploy.sh b/deploy.sh index 5a3427a..c9572c3 100755 --- a/deploy.sh +++ b/deploy.sh @@ -138,6 +138,12 @@ if [ "$GREENFIELD" = true ]; then # Copy apps.yaml template cp "$SCRIPT_DIR/config/nextworkspace/apps.yaml" "$TARGET_DIR/config/nextworkspace/apps.yaml" 2>/dev/null || true + # Extract Authelia secret for binary + AUTHELIA_SECRET=$(grep -oP 'session_secret: \K.*' "$TARGET_DIR/config/authelia/configuration.yml" 2>/dev/null || echo "") + if [ -n "$AUTHELIA_SECRET" ]; then + echo "AUTHELIA_SECRET=$AUTHELIA_SECRET" >> "$BACKUP_DIR/.env" + fi + # Write systemd service cat > /etc/systemd/system/$SERVICE_NAME.service < @@ -135,7 +226,7 @@ const landingPageHTML = ` - NextWorkspace — Your Self-Hosted Workspace for Startups + NextWorkspace @@ -207,9 +273,7 @@ const landingPageHTML = `

NextWorkspace was developed with assistance from AI coding tools, using DeepSeek as the provider and OpenCode as the development framework.

-
- © 2026 NextWorkspace — nextwks.eu -
+
© 2026 NextWorkspace — nextwks.eu
` @@ -227,57 +291,16 @@ const launcherHTML = ` color: #1a1a2e; min-height: 100vh; } - header { - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); - color: #fff; - padding: 2rem; - text-align: center; - } + header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: #fff; padding: 2rem; text-align: center; } header h1 { font-size: 2rem; margin-bottom: 0.25rem; } header p { color: #a0aec0; font-size: 1rem; } - .user-banner { - background: #2d3748; - color: #e2e8f0; - padding: 0.75rem 2rem; - text-align: center; - font-size: 0.9rem; - } - .user-banner a { color: #63b3ed; text-decoration: none; margin-left: 0.5rem; } + .user-banner { background: #2d3748; color: #e2e8f0; padding: 0.75rem 2rem; text-align: center; font-size: 0.9rem; display: flex; justify-content: center; gap: 1rem; } + .user-banner a { color: #63b3ed; text-decoration: none; } .user-banner a:hover { text-decoration: underline; } - .grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 1.5rem; - padding: 2rem; - max-width: 1200px; - margin: 0 auto; - } - .card { - background: #fff; - border-radius: 12px; - padding: 1.5rem; - text-align: center; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: transform 0.2s, box-shadow 0.2s; - text-decoration: none; - color: inherit; - display: block; - } - .card:hover { - transform: translateY(-4px); - box-shadow: 0 8px 24px rgba(0,0,0,0.12); - } - .icon { - width: 48px; - height: 48px; - margin: 0 auto 1rem; - background: #edf2f7; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.5rem; - } + .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1200px; margin: 0 auto; } + .card { background: #fff; border-radius: 12px; padding: 1.5rem; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.08); transition: transform 0.2s; text-decoration: none; color: inherit; display: block; } + .card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.12); } + .icon { width: 48px; height: 48px; margin: 0 auto 1rem; background: #edf2f7; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; } .card h3 { font-size: 1.1rem; margin-bottom: 0.25rem; } .card p { color: #718096; font-size: 0.85rem; } @@ -288,7 +311,9 @@ const launcherHTML = `

{{.Description}}

- Welcome, {{.User}} · Logout + Welcome, {{.User}} + {{if .IsAdmin}}Admin Panel{{end}} + Logout
{{range .Apps}} @@ -306,6 +331,88 @@ const launcherHTML = ` ` +const adminHTML = ` + + + + + Admin Panel — NextWorkspace + + + +
+

Admin Panel

+ Back to Launcher +
+
+ {{if .ApiError}}
Could not fetch users from authelia-api (is it running on :8080?)
{{end}} + +
+

Create User

+
+
+ + + + + + +
+
+
+ +
+

Users

+
{{.Users}}
+
+
+ + + +` + // --- Main --- func main() { @@ -328,23 +435,27 @@ func main() { mux := http.NewServeMux() - // Public paths + // Public mux.HandleFunc("/health", healthHandler) // Protected: launcher mux.Handle("/home/", authMiddleware(launcherHandler(cfg, apps))) mux.Handle("/home", authMiddleware(launcherHandler(cfg, apps))) + // Protected: admin — admins only + mux.Handle("/config", adminGroupMiddleware(authMiddleware(adminHandler))) + mux.Handle("/config/", adminGroupMiddleware(authMiddleware(adminHandler))) + // Protected: upstream app proxies for _, app := range apps { - if app.Path != "" && app.Upstream != "" { + if app.Path != "" && app.Upstream != "" && app.Path != "/config" && app.Path != "/home" { proxyHandler := authMiddleware(proxyToUpstream(app.Upstream)) mux.Handle(app.Path+"/", proxyHandler) mux.Handle(app.Path, proxyHandler) } } - // Default: serve landing page for www, redirect to launcher otherwise + // Default: www landing page or redirect to /home/ domain := os.Getenv("DOMAIN") mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.Host, "www.") || (domain != "" && r.Host == "www."+domain) { diff --git a/tools/manage-users.sh b/tools/manage-users.sh new file mode 100644 index 0000000..f1bd5a2 --- /dev/null +++ b/tools/manage-users.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Manage users via authelia-api +# Usage: ./manage-users.sh list +# ./manage-users.sh create username displayname email groups... +# ./manage-users.sh delete username + +API_BASE="http://127.0.0.1:8080" +TOKEN=$(grep -oP 'session_secret: \K.*' /opt/nextworkspace/config/authelia/configuration.yml) + +case "${1:-}" in + list) + curl -s -H "Authorization: Bearer $TOKEN" "$API_BASE/api/users" | jq . 2>/dev/null || \ + curl -s -H "Authorization: Bearer $TOKEN" "$API_BASE/api/users" + ;; + create) + shift + if [ $# -lt 3 ]; then + echo "Usage: $0 create username displayname email [groups...]" >&2 + exit 1 + fi + USERNAME="$1"; DISPLAY="$2"; EMAIL="$3"; shift 3 + GROUPS='["users"' + for g in "$@"; do GROUPS="$GROUPS,\"$g\""; done + GROUPS="$GROUPS]" + curl -s -X POST -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"users\":[{\"username\":\"$USERNAME\",\"display_name\":\"$DISPLAY\",\"email\":\"$EMAIL\",\"groups\":$GROUPS}]}" \ + "$API_BASE/api/users/bulk" + ;; + delete) + if [ -z "${2:-}" ]; then + echo "Usage: $0 delete username" >&2 + exit 1 + fi + curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \ + "$API_BASE/api/users/$2" + ;; + *) + echo "Usage: $0 {list|create|delete} ..." >&2 + exit 1 + ;; +esac