diff --git a/Makefile b/Makefile index 522f684..dd1be83 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ $(wildcard all-apps/nextcloud/*) \ $(wildcard all-apps/wg-easy/*) \ $(wildcard all-apps/ghost/*) \ $(wildcard all-apps/nassella/*) \ +all-apps/nassella/authelia-config/configuration.yml \ +all-apps/nassella/lldap-config/lldap_config.toml \ $(wildcard all-apps/dozzle/*) rm -Rf app/ @@ -46,6 +48,32 @@ all-apps/nextcloud/redis_password: $(apps_config) all-apps/nextcloud/nextcloud.env: $(apps_config) all-apps/nextcloud/nextcloud.env.tmpl make-nextcloud-env.sh ./make-nextcloud-env.sh $(apps_config) +# Nassella +all-apps/nassella/postgres_db: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_POSTGRES_DB" > $@' +all-apps/nassella/postgres_user: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_POSTGRES_USER" > $@' +all-apps/nassella/postgres_password: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_POSTGRES_PASSWORD" > $@' +all-apps/nassella/authelia_postgres_db: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_AUTHELIA_POSTGRES_DB" > $@' +all-apps/nassella/authelia_postgres_user: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_AUTHELIA_POSTGRES_USER" > $@' +all-apps/nassella/authelia_postgres_password: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_AUTHELIA_POSTGRES_PASSWORD" > $@' +all-apps/nassella/lldap_postgres_db: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_LLDAP_POSTGRES_DB" > $@' +all-apps/nassella/lldap_postgres_user: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_LLDAP_POSTGRES_USER" > $@' +all-apps/nassella/lldap_postgres_password: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_LLDAP_POSTGRES_PASSWORD" > $@' +all-apps/nassella/lldap_admin_password: $(apps_config) + bash -c 'source ./$(apps_config); printf "%s\n" "$$NASSELLA_LLDAP_ADMIN_PASSWORD" > $@' +all-apps/nassella/authelia-config/configuration.yml: $(apps_config) all-apps/nassella/authelia-config/configuration.yml.tmpl make-nassella-authelia-config.sh + ./make-nassella-authelia-config.sh $(apps_config) +all-apps/nassella/lldap-config/lldap_config.toml: $(apps_config) all-apps/nassella/lldap-config/lldap_config.toml.tmpl make-nassella-lldap-config.sh + ./make-nassella-lldap-config.sh $(apps_config) + # Ghost all-apps/ghost/.compose-env: $(apps_config) all-apps/ghost/.compose.env.tmpl make-ghost-env.sh ./make-ghost-env.sh $(apps_config) @@ -68,7 +96,15 @@ all-apps/nextcloud/nextcloud.env \ all-apps/nassella/postgres_db \ all-apps/nassella/postgres_user \ all-apps/nassella/postgres_password \ +all-apps/nassella/lldap_postgres_db \ +all-apps/nassella/lldap_postgres_user \ +all-apps/nassella/lldap_postgres_password \ +all-apps/nassella/authelia_postgres_db \ +all-apps/nassella/authelia_postgres_user \ +all-apps/nassella/authelia_postgres_password \ all-apps/nassella/nassella.env \ +all-apps/nassella/authelia-config/configuration.yml \ +all-apps/nassella/lldap-config/lldap_config.toml \ all-apps/ghost/.compose-env \ restic-env \ restic-password \ @@ -109,7 +145,7 @@ restic-snapshots: $(apps_config) restic-password archive: tar -cf nassella-latest.tar all-apps cl.yaml init-restic.sh main.tf make-caddyfile.sh Makefile \ make-generated.sh make-nextcloud-env.sh make-ghost-env.sh make-restic-generated.sh make-restic-password.sh restic-snapshots.sh copy-apps.sh \ - .terraform.lock.hcl + make-nassella-authelia-config.sh make-nassella-lldap-config.sh .terraform.lock.hcl cp nassella-latest.tar src/ ## to help me remember the command to run to test the config locally diff --git a/all-apps/nassella/authelia-config/configuration.yml.tmpl b/all-apps/nassella/authelia-config/configuration.yml.tmpl new file mode 100644 index 0000000..bc3a7f9 --- /dev/null +++ b/all-apps/nassella/authelia-config/configuration.yml.tmpl @@ -0,0 +1,119 @@ +--- +############################################################### +# Authelia configuration # +############################################################### + +server: + address: 'tcp://:9091/authelia' + endpoints: + authz: + forward-auth: + implementation: 'ForwardAuth' + +log: + level: 'debug' + +totp: + issuer: 'authelia.com' + +identity_validation: + reset_password: + jwt_secret: '$NASSELLA_AUTHELIA_JWT_SECRET' + +# lldap service account user should instead +# use an account with lldap_password_manager group +# since that can't be used to change an admin password +authentication_backend: + ldap: + address: 'ldap://nassella_lldap:3890' + implementation: 'lldap' + timeout: '5s' + pooling: + enable: false + count: 5 + retries: 2 + timeout: '10 seconds' + base_dn: 'DC=nassella,DC=org' +# additional_users_dn: 'OU=users' +# additional_groups_dn: 'OU=groups' +# group_search_mode: 'filter' +# permit_referrals: false + permit_unauthenticated_bind: false + permit_feature_detection_failure: false + user: 'uid=admin,ou=people,dc=nassella,dc=org' + password: '$NASSELLA_LLDAP_ADMIN_PASSWORD' + # attributes: + # distinguished_name: 'distinguishedName' + # username: 'uid' + # display_name: 'displayName' + # family_name: 'sn' + # given_name: 'givenName' + # middle_name: 'middleName' + # nickname: '' + # gender: '' + # birthdate: '' + # website: 'wWWHomePage' + # profile: '' + # picture: '' + # zoneinfo: '' + # locale: '' + # phone_number: 'telephoneNumber' + # phone_extension: '' + # street_address: 'streetAddress' + # locality: 'l' + # region: 'st' + # postal_code: 'postalCode' + # country: 'c' + # mail: 'mail' + # member_of: 'memberOf' + # group_name: 'cn' + # extra: + # extra_example: + # name: '' + # multi_valued: false + # value_type: 'string' + +access_control: + default_policy: 'deny' + rules: + # - domain: 'public.x.localhost' + # policy: 'bypass' + # - domain: 'app.nassella.org' + # policy: 'one_factor' + - domain: '$NASSELLA_FULL_DOMAIN' + policy: 'two_factor' + +session: + secret: 'insecure_session_secret' + + cookies: + - name: 'authelia_session' + domain: '$NASSELLA_FULL_DOMAIN' # Should match whatever your root protected domain is + authelia_url: 'https://$NASSELLA_FULL_DOMAIN/authelia' + expiration: '1 hour' # 1 hour + inactivity: '5 minutes' # 5 minutes + default_redirection_url: 'https://$NASSELLA_FULL_DOMAIN/dashboard' + +regulation: + max_retries: 3 + find_time: '2 minutes' + ban_time: '5 minutes' + +storage: + encryption_key: '$NASSELLA_AUTHELIA_KEY_SEED' + postgres: + address: 'tcp://nassella_authelia_db:5432' + servers: [] + database: 'authelia' + schema: 'public' + username: 'authelia' + password: '$NASSELLA_AUTHELIA_POSTGRES_PASSWORD' + timeout: '5s' + +notifier: + smtp: + address: 'submission://$SMTP_HOST:$SMTP_PORT' + username: '$SMTP_AUTH_USER' + password: '$SMTP_AUTH_PASSWORD' + sender: '$SMTP_FROM' +... diff --git a/all-apps/nassella/authelia_postgres_db b/all-apps/nassella/authelia_postgres_db new file mode 100644 index 0000000..5481933 --- /dev/null +++ b/all-apps/nassella/authelia_postgres_db @@ -0,0 +1 @@ +authelia \ No newline at end of file diff --git a/all-apps/nassella/authelia_postgres_password b/all-apps/nassella/authelia_postgres_password new file mode 100644 index 0000000..e69de29 diff --git a/all-apps/nassella/authelia_postgres_user b/all-apps/nassella/authelia_postgres_user new file mode 100644 index 0000000..5481933 --- /dev/null +++ b/all-apps/nassella/authelia_postgres_user @@ -0,0 +1 @@ +authelia \ No newline at end of file diff --git a/all-apps/nassella/docker-compose.yaml b/all-apps/nassella/docker-compose.yaml index 25e0256..7890862 100644 --- a/all-apps/nassella/docker-compose.yaml +++ b/all-apps/nassella/docker-compose.yaml @@ -7,8 +7,100 @@ secrets: file: ./nassella/postgres_password nassella_postgres_user: file: ./nassella/postgres_user + nassella_lldap_postgres_db: + file: ./nassella/lldap_postgres_db + nassella_lldap_postgres_password: + file: ./nassella/lldap_postgres_password + nassella_lldap_postgres_user: + file: ./nassella/lldap_postgres_user + nassella_authelia_postgres_db: + file: ./nassella/authelia_postgres_db + nassella_authelia_postgres_password: + file: ./nassella/authelia_postgres_password + nassella_authelia_postgres_user: + file: ./nassella/authelia_postgres_user + nassella_lldap_admin_password: + file: ./nassella/lldap_admin_password services: + nassella_lldap_db: + image: postgres:17.6-trixie + environment: + - POSTGRES_DB_FILE=/run/secrets/nassella_lldap_postgres_db + - POSTGRES_USER_FILE=/run/secrets/nassella_lldap_postgres_user + - POSTGRES_PASSWORD_FILE=/run/secrets/nassella_lldap_postgres_password + shm_size: 128mb + restart: always + volumes: + - /nassella/nassella/lldap-var-lib-postgresql-data:/var/lib/postgresql/data + networks: + - nassella_internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -d `cat $$POSTGRES_DB_FILE` -U `cat $$POSTGRES_USER_FILE`"] + start_period: 15s + interval: 30s + retries: 3 + timeout: 5s + secrets: + - nassella_lldap_postgres_db + - nassella_lldap_postgres_password + - nassella_lldap_postgres_user + + nassella_lldap: + image: lldap/lldap:stable + volumes: + - ./nassella/lldap-config/:/data + networks: + - lb + - nassella_internal + depends_on: + nassella_lldap_db: + condition: service_healthy + secrets: + - nassella_lldap_postgres_db + - nassella_lldap_postgres_password + - nassella_lldap_postgres_user + + nassella_authelia_db: + image: postgres:17.6-trixie + environment: + - POSTGRES_DB_FILE=/run/secrets/nassella_authelia_postgres_db + - POSTGRES_USER_FILE=/run/secrets/nassella_authelia_postgres_user + - POSTGRES_PASSWORD_FILE=/run/secrets/nassella_authelia_postgres_password + shm_size: 128mb + restart: always + volumes: + - /nassella/nassella/authelia-var-lib-postgresql-data:/var/lib/postgresql/data + networks: + - nassella_internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -d `cat $$POSTGRES_DB_FILE` -U `cat $$POSTGRES_USER_FILE`"] + start_period: 15s + interval: 30s + retries: 3 + timeout: 5s + secrets: + - nassella_authelia_postgres_db + - nassella_authelia_postgres_password + - nassella_authelia_postgres_user + + nassella_authelia: + image: 'authelia/authelia' + volumes: + - ./nassella/authelia-config/:/config + networks: + - lb + - nassella_internal + depends_on: + nassella_lldap: + condition: service_healthy + nassella_authelia_db: + condition: service_healthy + restart: 'unless-stopped' + healthcheck: + ## In production the healthcheck section should be commented. + disable: true + nassella_db: image: postgres:17.6-trixie env_file: @@ -40,6 +132,7 @@ services: - nassella_postgres_db - nassella_postgres_password - nassella_postgres_user + - nassella_lldap_admin_password networks: - lb - nassella_internal diff --git a/all-apps/nassella/lldap-config/lldap_config.toml.tmpl b/all-apps/nassella/lldap-config/lldap_config.toml.tmpl new file mode 100644 index 0000000..d2f98c0 --- /dev/null +++ b/all-apps/nassella/lldap-config/lldap_config.toml.tmpl @@ -0,0 +1,161 @@ +## Default configuration for Docker. +## All the values can be overridden through environment variables, prefixed +## with "LLDAP_". For instance, "ldap_port" can be overridden with the +## "LLDAP_LDAP_PORT" variable. + +## Tune the logging to be more verbose by setting this to be true. +## You can set it with the LLDAP_VERBOSE environment variable. +# verbose=false + +## The host address that the LDAP server will be bound to. +## To enable IPv6 support, simply switch "ldap_host" to "::": +## To only allow connections from localhost (if you want to restrict to local self-hosted services), +## change it to "127.0.0.1" ("::1" in case of IPv6). +## If LLDAP server is running in docker, set it to "0.0.0.0" ("::" for IPv6) to allow connections +## originating from outside the container. +ldap_host = "0.0.0.0" + +## The port on which to have the LDAP server. +#ldap_port = 3890 + +## The host address that the HTTP server will be bound to. +## To enable IPv6 support, simply switch "http_host" to "::". +## To only allow connections from localhost (if you want to restrict to local self-hosted services), +## change it to "127.0.0.1" ("::1" in case of IPv6). +## If LLDAP server is running in docker, set it to "0.0.0.0" ("::" for IPv6) to allow connections +## originating from outside the container. +http_host = "0.0.0.0" + +## The port on which to have the HTTP server, for user login and +## administration. +#http_port = 17170 + +## The public URL of the server, for password reset links. +http_url = "https://$NASSELLA_FULL_LLDAP_DOMAIN" + +## The path to the front-end assets (relative to the working directory). +#assets_path = "./app" + +## Random secret for JWT signature. +## This secret should be random, and should be shared with application +## servers that need to consume the JWTs. +## Changing this secret will invalidate all user sessions and require +## them to re-login. +## You should probably set it through the LLDAP_JWT_SECRET environment +## variable from a secret ".env" file. +## This can also be set from a file's contents by specifying the file path +## in the LLDAP_JWT_SECRET_FILE environment variable +## You can generate it with (on linux): +## LC_ALL=C tr -dc 'A-Za-z0-9!#%&'\''()*+,-./:;<=>?@[\]^_{|}~' . +from="$SMTP_FROM" +## Same for reply-to, optional. +#reply_to="Do not reply " + +## Options to configure LDAPS. +## To set these options from environment variables, use the following format +## (example with "port"): LLDAP_LDAPS_OPTIONS__PORT +[ldaps_options] +## Whether to enable LDAPS. +#enabled=true +## Port on which to listen. +#port=6360 +## Certificate file. +#cert_file="/data/cert.pem" +## Certificate key file. +#key_file="/data/key.pem" \ No newline at end of file diff --git a/all-apps/nassella/lldap_admin_password b/all-apps/nassella/lldap_admin_password new file mode 100644 index 0000000..e69de29 diff --git a/all-apps/nassella/lldap_postgres_db b/all-apps/nassella/lldap_postgres_db new file mode 100644 index 0000000..416339d --- /dev/null +++ b/all-apps/nassella/lldap_postgres_db @@ -0,0 +1 @@ +lldap \ No newline at end of file diff --git a/all-apps/nassella/lldap_postgres_password b/all-apps/nassella/lldap_postgres_password new file mode 100644 index 0000000..e69de29 diff --git a/all-apps/nassella/lldap_postgres_user b/all-apps/nassella/lldap_postgres_user new file mode 100644 index 0000000..416339d --- /dev/null +++ b/all-apps/nassella/lldap_postgres_user @@ -0,0 +1 @@ +lldap \ No newline at end of file diff --git a/main.tf b/main.tf index b126232..e8cb296 100644 --- a/main.tf +++ b/main.tf @@ -104,7 +104,7 @@ resource "digitalocean_reserved_ip" "machine" { resource "cloudflare_dns_record" "root" { zone_id = var.cloudflare_zone_id - name = "_nassella-instance" + name = "nassella-instance" content = digitalocean_reserved_ip.machine.ip_address type = "A" proxied = false @@ -115,7 +115,7 @@ resource "cloudflare_dns_record" "subdomains" { for_each = toset(var.subdomains) zone_id = var.cloudflare_zone_id name = each.key - content = "_nassella-instance.${var.domain}" + content = "nassella-instance.${var.domain}" type = "CNAME" proxied = false ttl = 300 diff --git a/make-caddyfile.sh b/make-caddyfile.sh index 59314ad..7086dd7 100755 --- a/make-caddyfile.sh +++ b/make-caddyfile.sh @@ -26,7 +26,26 @@ declare -A bodys bodys["nextcloud"]=" reverse_proxy http://nextcloud:80" bodys["wg-easy"]=" reverse_proxy http://wg-easy:80" bodys["ghost"]=" reverse_proxy http://ghost:2368" -bodys["nassella"]=" reverse_proxy http://nassella:8080" +bodys["nassella"]=$(cat < all-apps/nassella/authelia-config/configuration.yml + +# write secrets +echo "$NASSELLA_POSTGRES_DB" > all-apps/nassella/postgres_db +echo "$NASSELLA_POSTGRES_USER" > all-apps/nassella/postgres_user +echo "$NASSELLA_POSTGRES_PASSWORD" > all-apps/nassella/postgres_password + +echo "$NASSELLA_AUTHELIA_POSTGRES_DB" > all-apps/nassella/authelia_postgres_db +echo "$NASSELLA_AUTHELIA_POSTGRES_USER" > all-apps/nassella/authelia_postgres_user +echo "$NASSELLA_AUTHELIA_POSTGRES_PASSWORD" > all-apps/nassella/authelia_postgres_password + +echo "$NASSELLA_LLDAP_POSTGRES_DB" > all-apps/nassella/lldap_postgres_db +echo "$NASSELLA_LLDAP_POSTGRES_USER" > all-apps/nassella/lldap_postgres_user +echo "$NASSELLA_LLDAP_POSTGRES_PASSWORD" > all-apps/nassella/lldap_postgres_password diff --git a/make-nassella-lldap-config.sh b/make-nassella-lldap-config.sh new file mode 100755 index 0000000..686f4a3 --- /dev/null +++ b/make-nassella-lldap-config.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e +set -a # export everything in the config for later use by envsubst + +. $1 # source the apps.config file with then env vars + +read -r -a APP_CONFIGS <<< "$APP_CONFIGS" + +nassella_subdomain= + +for config_string in ${APP_CONFIGS[@]}; do + IFS=',' + read -r -a config <<< "$config_string" + + app=${config[0]} + subdomain=${config[1]} + + if [ "$app" = "nassella" ]; then + nassella_subdomain="$subdomain" + fi +done + +export NASSELLA_FULL_DOMAIN="$nassella_subdomain.$ROOT_DOMAIN" +export NASSELLA_FULL_LLDAP_DOMAIN="$NASSELLA_LLDAP_SUBDOMAIN.$nassella_subdomain.$ROOT_DOMAIN" +envsubst < all-apps/nassella/lldap-config/lldap_config.toml.tmpl > all-apps/nassella/lldap-config/lldap_config.toml + +# write secrets +echo "$NASSELLA_POSTGRES_DB" > all-apps/nassella/postgres_db +echo "$NASSELLA_POSTGRES_USER" > all-apps/nassella/postgres_user +echo "$NASSELLA_POSTGRES_PASSWORD" > all-apps/nassella/postgres_password + +echo "$NASSELLA_AUTHELIA_POSTGRES_DB" > all-apps/nassella/authelia_postgres_db +echo "$NASSELLA_AUTHELIA_POSTGRES_USER" > all-apps/nassella/authelia_postgres_user +echo "$NASSELLA_AUTHELIA_POSTGRES_PASSWORD" > all-apps/nassella/authelia_postgres_password + +echo "$NASSELLA_LLDAP_POSTGRES_DB" > all-apps/nassella/lldap_postgres_db +echo "$NASSELLA_LLDAP_POSTGRES_USER" > all-apps/nassella/lldap_postgres_user +echo "$NASSELLA_LLDAP_POSTGRES_PASSWORD" > all-apps/nassella/lldap_postgres_password diff --git a/src/Dockerfile b/src/Dockerfile index 5573a35..0b7cddc 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -52,7 +52,7 @@ RUN chmod +x nassella-run FROM debian:trixie-slim RUN apt-get update && apt-get -y --no-install-recommends install \ - libpq-dev \ + libpq-dev ca-certificates gettext-base \ && rm -rf /var/lib/apt/lists/* COPY --from=buildeggs /usr/local/ /usr/local/ diff --git a/src/db-clean.sql b/src/db-clean.sql index 667c38d..1daa123 100644 --- a/src/db-clean.sql +++ b/src/db-clean.sql @@ -18,5 +18,5 @@ drop table user_service_configs; drop index instances_user_id_instance_id_idx; drop table instances; -drop index users_auth_user_id_idx; +drop index users_username_idx; drop table users; diff --git a/src/db-init.sql b/src/db-init.sql index 7d08f54..b3e2d42 100644 --- a/src/db-init.sql +++ b/src/db-init.sql @@ -1,12 +1,11 @@ create table users( user_id bigserial primary key, - auth_user_id int unique not null, email varchar(255) not null, username varchar(255) not null unique, key_key varchar(255), key_iv varchar(255) ); -create unique index users_auth_user_id_idx on users (auth_user_id); +create unique index users_username_idx on users (username); create table instances( instance_id bigserial primary key, diff --git a/src/db.scm b/src/db.scm index bf5ddb9..41a8b4f 100644 --- a/src/db.scm +++ b/src/db.scm @@ -7,7 +7,8 @@ db-init db-clean create-user delete-user - create-instance get-user-instances + get-user-id-by-username + create-instance destroy-instance get-user-instances get-instance-ssh-pub-key get-instance-ssh-priv-key update-instance-ssh-pub-key get-instance-restic-password @@ -41,10 +42,17 @@ crypto-tools spiffy) -(define connection-spec (make-parameter '((dbname . "nassella") (user . "nassella") (password . "password") - ;; (host . "127.0.0.1") - (host . "nassella_db") - ))) +(define connection-spec + (make-parameter + (cond-expand + (dev + '((dbname . "nassella") (user . "nassella") (password . "password") + (host . "127.0.0.1"))) + (else + (let ((pw (string-trim-right (with-input-from-file "/run/secrets/nassella_postgres_password" read-string)))) ;; remove newline + `((dbname . "nassella") (user . "nassella") (password . ,pw) + (host . "nassella_db"))))))) + (define db-connection (make-parameter #f)) (define (with-db proc) @@ -101,7 +109,7 @@ (define *root-key-key* (ensure-root-key)) (define (get-user-key-and-iv conn user-id) - (row-alist (query conn "select auth_user_id, key_key, key_iv from users where user_id=$1;" user-id))) + (row-alist (query conn "select username, key_key, key_iv from users where user_id=$1;" user-id))) (define (get-decrypted-user-key-and-iv conn user-id) (let* ((auth-user-id-and-user-key-and-iv (get-user-key-and-iv conn user-id)) @@ -109,9 +117,9 @@ (raw-user-key (hexstring->blob (string-drop-right raw-user-key-and-tag (* tag-length 2)))) (raw-user-tag (hexstring->blob (string-take-right raw-user-key-and-tag (* tag-length 2)))) (user-key (decrypt (blob->string raw-user-key) (blob->string raw-user-tag) *root-key-key* *root-key-iv* - (string->blob (number->string (alist-ref 'auth_user_id auth-user-id-and-user-key-and-iv))))) + (string->blob (alist-ref 'username auth-user-id-and-user-key-and-iv)))) (user-iv (alist-ref 'key_iv auth-user-id-and-user-key-and-iv)) - (auth-user-id (alist-ref 'auth_user_id auth-user-id-and-user-key-and-iv))) + (auth-user-id (alist-ref 'username auth-user-id-and-user-key-and-iv))) (values (hexstring->blob user-key) (hexstring->blob user-iv) auth-user-id))) (define (user-encrypt message user-key user-iv user-id) @@ -131,17 +139,17 @@ (raw-tag (hexstring->blob (string-take-right message-and-tag (* tag-length 2))))) (user-decrypt (blob->string raw-message) (blob->string raw-tag) user-key user-iv user-id))) -(define (create-user conn auth-user-id email username) +(define (create-user conn email username) (let ((user-key (blob->hexstring/uppercase (generate-key))) (user-iv (blob->hexstring/uppercase (generate-iv)))) (receive (enc-user-key tag) - (encrypt user-key *root-key-key* *root-key-iv* (string->blob (number->string auth-user-id))) + (encrypt user-key *root-key-key* *root-key-iv* (string->blob username)) (let ((user-id (value-at (query conn - "insert into users(auth_user_id, email, username, key_key, key_iv) values ($1, $2, $3, $4, $5) + "insert into users(email, username, key_key, key_iv) values ($1, $2, $3, $4) returning users.user_id;" - auth-user-id email username + email username (string-append (blob->hexstring/uppercase (string->blob enc-user-key)) (blob->hexstring/uppercase (string->blob tag))) user-iv)))) @@ -150,6 +158,12 @@ returning users.user_id;" (define (delete-user conn user-id) (query conn "delete from users where user_id=$1;" user-id)) +(define (get-user-id-by-username conn username) + (let ((res (query conn "select user_id from users where username=$1;" username))) + (if (> (row-count res) 0) + (value-at res) + #f))) + ;; We also encrypt the ssh pub key not to hide it but to make it ;; more difficult for someone to tamper with it which could allow ;; an attacker to poison an instance with an ssh key that they have @@ -171,6 +185,9 @@ returning users.user_id;" (query conn "insert into user_terraform_state(user_id, instance_id) values ($1, $2);" user-id instance-id) instance-id))) +(define (destroy-instance conn instance-id) + (query conn "delete from instances where instance_id=$1;" instance-id)) + (define (get-instance-ssh-priv-key conn user-id instance-id) (receive (user-key user-iv auth-user-id) (get-decrypted-user-key-and-iv conn user-id) @@ -386,7 +403,10 @@ returning users.user_id;" (value-at (query conn "select status from deployments where id=$1;" deployment-id))) (define (get-most-recent-deployment-status conn user-id instance-id) - (value-at (query conn "select status from deployments where user_id=$1 and instance_id=$2 order by id DESC limit 1;" user-id instance-id))) + (let ((res (query conn "select status from deployments where user_id=$1 and instance_id=$2 order by id DESC limit 1;" user-id instance-id))) + (if (> (row-count res) 0) + (value-at res) + #f))) (define *deployments-column-map* '((generate-configs . "generate_configs") @@ -550,7 +570,7 @@ returning users.user_id;" (string-split (with-input-from-file "db-init.sql" read-string) ";")) (log-to (debug-log) "table creation finished") (log-to (debug-log) "creating test user") - (create-user db 1 "me@example.com" "username") + (create-user db "me@example.com" "username") (log-to (debug-log) "test user creation finished")))))) (define (db-clean) diff --git a/src/nassella.scm b/src/nassella.scm index 0c019d1..90adf54 100644 --- a/src/nassella.scm +++ b/src/nassella.scm @@ -298,7 +298,11 @@ h1, h2, h3, h4, h5, h6 { (define test-user-id (make-parameter 1)) (define (session-user-id) - (or (session-get "user-id") (test-user-id))) + (cond-expand + (dev + (or (session-get "user-id") (test-user-id))) + (else + (session-get "user-id")))) (define-syntax get/widgets (syntax-rules () @@ -314,9 +318,14 @@ h1, h2, h3, h4, h5, h6 { headers) ;; `((meta (@ (name "viewport") (content "width=device-width")))) (begin - ;; TODO remove once sessions are integrated - (session-set! "user-id" (test-user-id)) - (session-set! "username" "me") + (cond-expand + (dev + (session-set! "user-id" (test-user-id)) + (session-set! "username" "me")) + (else + (let ((user-id (with-db/transaction (lambda (db) (get-user-id-by-username db (header-value 'remote-user (request-headers (current-request)))))))) + (when user-id (session-set! "user-id" user-id)) + (session-set! "username" (header-value 'remote-user (request-headers (current-request))))))) body ...)))))))) (define-widget (Container ((max-width ($ 'width.main.max)) (style '())) contents) @@ -524,6 +533,57 @@ h1, h2, h3, h4, h5, h6 { (json-parsers (cons array-as-list-parser (json-parsers))) +;; TODO change username to to a prod API key that has read access +;; to the checkout session +(define (send-stripe-request #!key (method 'GET) endpoint (body #f) (username "")) + (define api-endpoint "https://api.stripe.com/") + (define api-version "/v1") + + (with-input-from-request + (make-request method: method + uri: (uri-reference (string-append api-endpoint api-version endpoint)) + headers: (headers `((authorization . (#(basic ((username . ,username) (password . "")))))))) + body + read-json)) + +(define (stripe-session-email sid) + (alist-ref + 'email + (alist-ref + 'customer_details + (send-stripe-request endpoint: (string-append "/checkout/sessions/" sid))))) + + +(define (create-lldap-user username email) + ;; query = mutation createUser($user:CreateUserInput!){createUser(user:$user){id email displayName firstName lastName avatar}} + ;; variables = {\"user\":{\"id\":\"${id}\",\"email\":\"${email}\",\"displayName\":\"${name}\",\"firstName\":\"${firstName}\",\"lastName\":\"${lastName}\",\"avatar\":\" + ;; data="{\"query\":\"${query}\",\"variables\":${variables}" + ;; http://localhost:17170/api/graphql + ;; -H 'Content-Type: application/json' \ + ;; -H "Authorization: Bearer $token" \ + (let ((api-token + (alist-ref + 'token + (with-input-from-request + (make-request method: 'POST + uri: (uri-reference "http://nassella_lldap:17170/auth/simple/login") + headers: (headers `((content-type application/json)))) + (lambda () + (write-json + `((username . "admin") (password . ,(string-trim-right (with-input-from-file "/run/secrets/nassella_lldap_admin_password" read-string)))))) ;; trim to remove newline + read-json)))) + (with-input-from-request + (make-request method: 'POST + uri: (uri-reference "http://nassella_lldap:17170/api/graphql") + headers: (headers `((content-type application/json) + (authorization #(,(string-append "Bearer " api-token) raw))))) + (lambda () + (write-json + `((query . "mutation createUser($user:CreateUserInput!){createUser(user:$user){id email displayName firstName lastName avatar}}") + (variables . ((user . ((id . ,username) + (email . ,email)))))))) + read-json))) + (define (get-digital-ocean-regions api-token) (filter (lambda (r) @@ -733,6 +793,21 @@ chmod -R 777 /opt/keys")) "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+={}[]|<>,.?") 30))) +(define (generate-jwt-secret) + (generator->string (gtake (make-random-char-generator + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+={}[]|<>,.?") + 32))) + +(define (generate-key-seed) + (generator->string (gtake (make-random-char-generator + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+={}[]|<>,.?") + 32))) + +(define (generate-authelia-key-seed) + (generator->string (gtake (make-random-char-generator + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + 64))) + (define (generate-postgres-password) (generator->string (gtake (make-random-char-generator "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") @@ -746,13 +821,34 @@ chmod -R 777 /opt/keys")) (with-schematra-app app (lambda () +;;; UNSECURED PAGES +(get/widgets + ("/unsecured/account/create") + `(App + (form + (@ (action "/unsecured/account/create-submit") (method POST)) + (VStack + (Fieldset + (@ (title "Account Details")) + (Field (@ (name "username") (label ("Username")))) + (input (@ (type "hidden") (name "sid") (value ,(alist-ref 'sid (current-params) equal?)))) + (Button (@ (type "submit")) "Create Account")))))) + +(post "/unsecured/account/create-submit" + (let ((email (stripe-session-email (alist-ref 'sid (current-params)))) + (username (alist-ref 'username (current-params)))) + (create-lldap-user username email) + (with-db/transaction (lambda (db) (create-user db email username)))) + (redirect "/authelia/reset-password")) + +;;; REQUIRES AUTHED USER (post "/config/wizard/create-instance" - (let* ((ssh-keys (generate-ssh-key (session-user-id))) - (instance-id (with-db/transaction - (lambda (db) - (create-instance db (session-user-id) (first ssh-keys) (second ssh-keys) - (generate-restic-password)))))) - (redirect (conc "/config/wizard/services/" instance-id)))) + (let* ((ssh-keys (generate-ssh-key (session-user-id))) + (instance-id (with-db/transaction + (lambda (db) + (create-instance db (session-user-id) (first ssh-keys) (second ssh-keys) + (generate-restic-password)))))) + (redirect (conc "/config/wizard/services/" instance-id)))) ;; TODO should all these key related form fields be of type password ;; so the browser doesn't save them??? @@ -957,7 +1053,11 @@ chmod -R 777 /opt/keys")) ,@(if (member 'nassella selected-apps) `((Fieldset (@ (title "Nassella")) - (Field (@ (name "nassella-subdomain") (label ("Subdomain")) (value ,(alist-ref 'subdomain (alist-ref 'nassella app-config eq? '()) eq? "nassella")))))) + (Field (@ (name "nassella-subdomain") (label ("Subdomain")) (value ,(alist-ref 'subdomain (alist-ref 'nassella app-config eq? '()) eq? "app")))) + (Field (@ (name "nassella-lldap-subdomain") (label ("LLDAP Subdomain")) + (value ,(alist-ref 'lldap-subdomain (alist-ref 'nassella app-config eq? '()) eq? "lldap")))) + (Field (@ (name "nassella-lldap-admin-password") (label ("Admin Password")) (type "password") + (value ,(alist-ref 'lldap-admin-password (alist-ref 'nassella app-config eq? '()) eq? "")))))) '()) (Fieldset (@ (title "Log Viewer")) @@ -967,7 +1067,7 @@ chmod -R 777 /opt/keys")) (value ,(alist-ref 'user (alist-ref 'log-viewer app-config eq? '()) eq? "")))) (Field (@ (name "log-viewer-password") (label ("Password")) (type "password") (value ,(alist-ref 'password (alist-ref 'log-viewer app-config eq? '()) eq? ""))))) - ,@(if (or (member 'nextcloud selected-apps) (member 'ghost selected-apps)) + ,@(if (or (member 'nextcloud selected-apps) (member 'ghost selected-apps) (member 'nassella selected-apps)) `((Fieldset (@ (title "All Apps - Email - SMTP")) (Field (@ (name "smtp-host") (label ("Host")) @@ -1009,7 +1109,30 @@ chmod -R 777 /opt/keys")) (redis-password . ,(or (alist-ref 'redis-password (alist-ref 'nextcloud config eq? '())) (generate-redis-password))))) - (nassella . ((subdomain . ,(alist-ref 'nassella-subdomain (current-params))))) + (nassella . ((subdomain . ,(alist-ref 'nassella-subdomain (current-params))) + (postgres-password . ,(or (alist-ref 'postgres-password + (alist-ref 'nassella config eq? '())) + (generate-postgres-password))) + (authelia-postgres-password . ,(or (alist-ref 'authelia-postgres-password + (alist-ref 'nassella config eq? '())) + (generate-postgres-password))) + (lldap-postgres-password . ,(or (alist-ref 'lldap-postgres-password + (alist-ref 'nassella config eq? '())) + (generate-postgres-password))) + (lldap-jwt-secret . ,(or (alist-ref 'lldap-jwt-secret + (alist-ref 'nassella config eq? '())) + (generate-jwt-secret))) + (lldap-key-seed . ,(or (alist-ref 'lldap-key-seed + (alist-ref 'nassella config eq? '())) + (generate-key-seed))) + (lldap-subdomain . ,(alist-ref 'nassella-lldap-subdomain (current-params))) + (lldap-admin-password . ,(alist-ref 'nassella-lldap-admin-password (current-params))) + (authelia-jwt-secret . ,(or (alist-ref 'authelia-jwt-secret + (alist-ref 'nassella config eq? '())) + (generate-jwt-secret))) + (authelia-key-seed . ,(or (alist-ref 'authelia-key-seed + (alist-ref 'nassella config eq? '())) + (generate-authelia-key-seed))))) (log-viewer . ((subdomain . ,(alist-ref 'log-viewer-subdomain (current-params))) (user . ,(alist-ref 'log-viewer-user (current-params))) (password . ,(alist-ref 'log-viewer-password (current-params))))) @@ -1117,7 +1240,8 @@ chmod -R 777 /opt/keys")) (@ (step "Review")) (h2 "Root Domain") ,root-domain - (h2 "Apps") + (h2 "Apps") ;; TODO if an app that was previously selected is now unselected we need to somehow delete its data + ;; so that if the user then re-deploys the app later we don't have key conflicts (ul ,@(map (lambda (app) `(li ,app " @ " ,(alist-ref 'subdomain (alist-ref app config)) "." @@ -1182,6 +1306,21 @@ chmod -R 777 /opt/keys")) ("NEXTCLOUD_REDIS_PASSWORD" . ,(alist-ref 'redis-password (alist-ref 'nextcloud config))) ("GHOST_DATABASE_ROOT_PASSWORD" . ,(alist-ref 'postgres-root-password (alist-ref 'ghost config))) ("GHOST_DATABASE_PASSWORD" . ,(alist-ref 'postgres-password (alist-ref 'ghost config))) + ("NASSELLA_LLDAP_SUBDOMAIN" . ,(alist-ref 'lldap-subdomain (alist-ref 'nassella config))) + ("NASSELLA_POSTGRES_DB" . "nassella") + ("NASSELLA_POSTGRES_USER" . "nassella") + ("NASSELLA_POSTGRES_PASSWORD" . ,(alist-ref 'postgres-password (alist-ref 'nassella config))) + ("NASSELLA_AUTHELIA_POSTGRES_DB" . "authelia") + ("NASSELLA_AUTHELIA_POSTGRES_USER" . "authelia") + ("NASSELLA_AUTHELIA_POSTGRES_PASSWORD" . ,(alist-ref 'authelia-postgres-password (alist-ref 'nassella config))) + ("NASSELLA_LLDAP_POSTGRES_DB" . "lldap") + ("NASSELLA_LLDAP_POSTGRES_USER" . "lldap") + ("NASSELLA_LLDAP_POSTGRES_PASSWORD" . ,(alist-ref 'lldap-postgres-password (alist-ref 'nassella config))) + ("NASSELLA_LLDAP_JWT_SECRET" . ,(alist-ref 'lldap-jwt-secret (alist-ref 'nassella config))) + ("NASSELLA_LLDAP_KEY_SEED" . ,(alist-ref 'lldap-key-seed (alist-ref 'nassella config))) + ("NASSELLA_LLDAP_ADMIN_PASSWORD" . ,(alist-ref 'lldap-admin-password (alist-ref 'nassella config))) + ("NASSELLA_AUTHELIA_JWT_SECRET" . ,(alist-ref 'authelia-jwt-secret (alist-ref 'nassella config))) + ("NASSELLA_AUTHELIA_KEY_SEED" . ,(alist-ref 'authelia-key-seed (alist-ref 'nassella config))) ("SMTP_HOST" . ,(alist-ref 'smtp-host (alist-ref 'all-apps config))) ("SMTP_PORT" . ,(alist-ref 'smtp-port (alist-ref 'all-apps config))) ("SMTP_AUTH_USER" . ,(alist-ref 'smtp-auth-user (alist-ref 'all-apps config))) @@ -1200,10 +1339,10 @@ chmod -R 777 /opt/keys")) ("cloudflare_api_token" . ,(alist-ref 'cloudflare-api-token service-config)) ("cloudflare_zone_id" . ,(alist-ref 'cloudflare-zone-id service-config)) ("cloudflare_account_id" . ,(alist-ref 'cloudflare-account-id service-config)) - ("cluster_name" . "mycluster") + ("cluster_name" . "nassella") ("datacenter" . ,(alist-ref 'digitalocean-region service-config)) ;; (source <(curl -sSfL https://stable.release.flatcar-linux.net/amd64-usr/current/version.txt); echo "${FLATCAR_VERSION_ID}") - ("flatcar_stable_version" . "4459.2.3"))) + ("flatcar_stable_version" . "4459.2.4"))) ;; remove the newline that generating the ssh key adds (display "ssh_keys=[\"") (display (string-drop-right ssh-pub-key 1)) (print "\"]")))) (let* ((instance-id (alist-ref "id" (current-params) equal?)) @@ -1346,7 +1485,9 @@ chmod -R 777 /opt/keys")) (li "Upgrade Now (pending automatic upgrades scheduled for: )") (li "Manage Backups") (li (a (@ (href "/destroy/" ,(alist-ref 'instance-id instance))) - "Destroy (confirmation required)"))))))) + "Destroy - deletes data and configuration (confirmation required)")) + (li (a (@ (href "/reset/" ,(alist-ref 'instance-id instance))) + "Reset - deletes data (confirmation required)"))))))) (with-db/transaction (lambda (db) (get-dashboard db (session-user-id)))))))))) @@ -1361,7 +1502,7 @@ chmod -R 777 /opt/keys")) `(App (h2 "Destroy Instance") ,root-domain - (h2 "This action is NOT reversible") + (h2 "This action is NOT reversible. All data will be lost!") (form (@ (action ,(conc "/destroy-submit/" instance-id)) (method POST)) (VStack @@ -1399,50 +1540,66 @@ chmod -R 777 /opt/keys")) (begin (setup-deploy-files dir (alist-ref 'state terraform-state) (alist-ref 'backup terraform-state)) (with-output-to-file (string-append dir "/config/apps.config") - (lambda () - (map (lambda (e) - (write-config-entry (car e) (cdr e))) - `(("ROOT_DOMAIN" . ,root-domain) - ("APP_CONFIGS" . ,(string-intersperse - (map (lambda (app) - (conc (if (eq? app 'log-viewer) 'dozzle app) - "," - (alist-ref 'subdomain (alist-ref app config)))) - selected-apps) - " ")) - ("HOST_ADMIN_USER" . ,(alist-ref 'user (alist-ref 'log-viewer config))) - ("HOST_ADMIN_PASSWORD" . ,(alist-ref 'password (alist-ref 'log-viewer config))) - ("NEXTCLOUD_ADMIN_USER" . ,(alist-ref 'admin-user (alist-ref 'nextcloud config))) - ("NEXTCLOUD_ADMIN_PASSWORD" . ,(alist-ref 'admin-password (alist-ref 'nextcloud config))) - ("NEXTCLOUD_POSTGRES_DB" . "nextcloud") - ("NEXTCLOUD_POSTGRES_USER" . "nextcloud") - ("NEXTCLOUD_POSTGRES_PASSWORD" . ,(alist-ref 'postgres-password (alist-ref 'nextcloud config))) - ("NEXTCLOUD_REDIS_PASSWORD" . ,(alist-ref 'redis-password (alist-ref 'nextcloud config))) - ("GHOST_DATABASE_ROOT_PASSWORD" . ,(alist-ref 'postgres-root-password (alist-ref 'ghost config))) - ("GHOST_DATABASE_PASSWORD" . ,(alist-ref 'postgres-password (alist-ref 'ghost config))) - ("SMTP_HOST" . ,(alist-ref 'smtp-host (alist-ref 'all-apps config))) - ("SMTP_PORT" . ,(alist-ref 'smtp-port (alist-ref 'all-apps config))) - ("SMTP_AUTH_USER" . ,(alist-ref 'smtp-auth-user (alist-ref 'all-apps config))) - ("SMTP_AUTH_PASSWORD" . ,(alist-ref 'smtp-auth-password (alist-ref 'all-apps config))) - ("SMTP_FROM" . ,(alist-ref 'smtp-from (alist-ref 'all-apps config))) - ("BACKBLAZE_KEY_ID" . ,(alist-ref 'backblaze-key-id service-config)) - ("BACKBLAZE_APPLICATION_KEY" . ,(alist-ref 'backblaze-application-key service-config)) - ("BACKBLAZE_BUCKET_URL" . ,(alist-ref 'backblaze-bucket-url service-config)) - ("RESTIC_PASSWORD" . ,restic-password))))) - (with-output-to-file (string-append dir "/config/production.tfvars") - (lambda () - (map (lambda (e) - (write-config-entry (car e) (cdr e))) - `(("server_type" . ,(alist-ref 'digitalocean-size service-config)) - ("do_token" . ,(alist-ref 'digitalocean-api-token service-config)) - ("cloudflare_api_token" . ,(alist-ref 'cloudflare-api-token service-config)) - ("cloudflare_zone_id" . ,(alist-ref 'cloudflare-zone-id service-config)) - ("cloudflare_account_id" . ,(alist-ref 'cloudflare-account-id service-config)) - ("cluster_name" . "mycluster") - ("datacenter" . ,(alist-ref 'digitalocean-region service-config)) - ("flatcar_stable_version" . "4459.2.1"))) - ;; remove the newline that generating the ssh key adds - (display "ssh_keys=[\"") (display (string-drop-right ssh-pub-key 1)) (print "\"]"))) + (lambda () + (map (lambda (e) + (write-config-entry (car e) (cdr e))) + `(("ROOT_DOMAIN" . ,root-domain) + ("APP_CONFIGS" . ,(string-intersperse + (map (lambda (app) + (conc (if (eq? app 'log-viewer) 'dozzle app) + "," + (alist-ref 'subdomain (alist-ref app config)))) + selected-apps) + " ")) + ("HOST_ADMIN_USER" . ,(alist-ref 'user (alist-ref 'log-viewer config))) + ("HOST_ADMIN_PASSWORD" . ,(alist-ref 'password (alist-ref 'log-viewer config))) + ("NEXTCLOUD_ADMIN_USER" . ,(alist-ref 'admin-user (alist-ref 'nextcloud config))) + ("NEXTCLOUD_ADMIN_PASSWORD" . ,(alist-ref 'admin-password (alist-ref 'nextcloud config))) + ("NEXTCLOUD_POSTGRES_DB" . "nextcloud") + ("NEXTCLOUD_POSTGRES_USER" . "nextcloud") + ("NEXTCLOUD_POSTGRES_PASSWORD" . ,(alist-ref 'postgres-password (alist-ref 'nextcloud config))) + ("NEXTCLOUD_REDIS_PASSWORD" . ,(alist-ref 'redis-password (alist-ref 'nextcloud config))) + ("GHOST_DATABASE_ROOT_PASSWORD" . ,(alist-ref 'postgres-root-password (alist-ref 'ghost config))) + ("GHOST_DATABASE_PASSWORD" . ,(alist-ref 'postgres-password (alist-ref 'ghost config))) + ("NASSELLA_LLDAP_SUBDOMAIN" . ,(alist-ref 'lldap-subdomain (alist-ref 'nassella config))) + ("NASSELLA_POSTGRES_DB" . "nassella") + ("NASSELLA_POSTGRES_USER" . "nassella") + ("NASSELLA_POSTGRES_PASSWORD" . ,(alist-ref 'postgres-password (alist-ref 'nassella config))) + ("NASSELLA_AUTHELIA_POSTGRES_DB" . "authelia") + ("NASSELLA_AUTHELIA_POSTGRES_USER" . "authelia") + ("NASSELLA_AUTHELIA_POSTGRES_PASSWORD" . ,(alist-ref 'authelia-postgres-password (alist-ref 'nassella config))) + ("NASSELLA_LLDAP_POSTGRES_DB" . "lldap") + ("NASSELLA_LLDAP_POSTGRES_USER" . "lldap") + ("NASSELLA_LLDAP_POSTGRES_PASSWORD" . ,(alist-ref 'lldap-postgres-password (alist-ref 'nassella config))) + ("NASSELLA_LLDAP_JWT_SECRET" . ,(alist-ref 'lldap-jwt-secret (alist-ref 'nassella config))) + ("NASSELLA_LLDAP_KEY_SEED" . ,(alist-ref 'lldap-key-seed (alist-ref 'nassella config))) + ("NASSELLA_LLDAP_ADMIN_PASSWORD" . ,(alist-ref 'lldap-admin-password (alist-ref 'nassella config))) + ("NASSELLA_AUTHELIA_JWT_SECRET" . ,(alist-ref 'authelia-jwt-secret (alist-ref 'nassella config))) + ("NASSELLA_AUTHELIA_KEY_SEED" . ,(alist-ref 'authelia-key-seed (alist-ref 'nassella config))) + ("SMTP_HOST" . ,(alist-ref 'smtp-host (alist-ref 'all-apps config))) + ("SMTP_PORT" . ,(alist-ref 'smtp-port (alist-ref 'all-apps config))) + ("SMTP_AUTH_USER" . ,(alist-ref 'smtp-auth-user (alist-ref 'all-apps config))) + ("SMTP_AUTH_PASSWORD" . ,(alist-ref 'smtp-auth-password (alist-ref 'all-apps config))) + ("SMTP_FROM" . ,(alist-ref 'smtp-from (alist-ref 'all-apps config))) + ("BACKBLAZE_KEY_ID" . ,(alist-ref 'backblaze-key-id service-config)) + ("BACKBLAZE_APPLICATION_KEY" . ,(alist-ref 'backblaze-application-key service-config)) + ("BACKBLAZE_BUCKET_URL" . ,(alist-ref 'backblaze-bucket-url service-config)) + ("RESTIC_PASSWORD" . ,restic-password))))) + (with-output-to-file (string-append dir "/config/production.tfvars") + (lambda () + (map (lambda (e) + (write-config-entry (car e) (cdr e))) + `(("server_type" . ,(alist-ref 'digitalocean-size service-config)) + ("do_token" . ,(alist-ref 'digitalocean-api-token service-config)) + ("cloudflare_api_token" . ,(alist-ref 'cloudflare-api-token service-config)) + ("cloudflare_zone_id" . ,(alist-ref 'cloudflare-zone-id service-config)) + ("cloudflare_account_id" . ,(alist-ref 'cloudflare-account-id service-config)) + ("cluster_name" . "nassella") + ("datacenter" . ,(alist-ref 'digitalocean-region service-config)) + ;; (source <(curl -sSfL https://stable.release.flatcar-linux.net/amd64-usr/current/version.txt); echo "${FLATCAR_VERSION_ID}") + ("flatcar_stable_version" . "4459.2.4"))) + ;; remove the newline that generating the ssh key adds + (display "ssh_keys=[\"") (display (string-drop-right ssh-pub-key 1)) (print "\"]"))) ;; TODO need a new table to track destroying? ;; as this is creating a new "deployment" ;; to attach state to @@ -1495,9 +1652,54 @@ chmod -R 777 /opt/keys")) (with-input-from-file (string-append dir "/make-out") read-string)) (update-user-terraform-state db user-id instance-id (if (eof-object? tf-state) "" tf-state) - (if (eof-object? tf-state-backup) "" tf-state-backup)))))))))))) + (if (eof-object? tf-state-backup) "" tf-state-backup)) + (when exit-normal + (destroy-instance db instance-id)))))))))))) (redirect (conc "/destroy-success/" (alist-ref "id" (current-params) equal?))))))) +(get/widgets + ("/destroy-success/:id" + (let* ((instance-id (alist-ref "id" (current-params) equal?)) + (res (with-db/transaction + (lambda (db) + `((status . ,(get-most-recent-deployment-status db (session-user-id) instance-id)))))) + (status (or (and (alist-ref 'status res) (string->symbol (alist-ref 'status res))) 'destroyed))) + (if (or (eq? status 'complete) (eq? status 'failed) (eq? status 'destroyed)) + '() + '((meta (@ (http-equiv "refresh") (content "5"))))))) + (let* ((instance-id (alist-ref "id" (current-params) equal?)) + (res (with-db/transaction + (lambda (db) + `((status . ,(get-most-recent-deployment-status db (session-user-id) instance-id)) + (progress . ,(get-most-recent-deployment-progress db (session-user-id) instance-id)))))) + (output (with-input-from-file (string-append (deployment-directory (session-user-id)) "/make-out") read-string)) + (progress (alist-ref 'progress res)) + (status (alist-ref 'status res))) + `(App + (Main-Container + (VStack + (h1 + ,(case (string->symbol status) + ((queued) "Destroy queued") + ((in-progress) "Destroy in progress") + ((destroyed) "Destroy complete!") + ((failed) "Destroy failed"))) + ,@(if (eq? status 'destroyed) + '((a (@ (href "/dashboard")) "Dashboard")) + `((ul (li "generate configs: " ,(progress-status->text (alist-ref 'generate-configs progress))) + (li "custom flatcar image: " ,(progress-status->text (alist-ref 'custom-image progress))) + (li "machine create: " ,(progress-status->text (alist-ref 'machine-create progress))) + (li "cleanup previous machine: " ,(progress-status->text (alist-ref 'machine-destroy progress)))) + (div + (a (@ (href "/dashboard")) "Dashboard") + ,@(if (or (eq? (string->symbol status) 'complete) (eq? (string->symbol status) 'failed)) + '() + " (deployment will continue in the background if you leave this page)")) + (hr) + (pre (@ (style ((overflow-x "scroll")))) + ,output))) + ))))) + (schematra-install) ))