This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

modules.zitadel: add an SMTP provider

lass.zitadel.smtp creates, updates and activates the instance's SMTP
provider. Zitadel 2.71 skips providers without a password, so the
password comes from passwordFile; a hash of the one Zitadel has is kept
in the state directory to update it when the file changes.

+64 -1
+22 -1
modules/zitadel/default.nix
··· 17 17 domain = zitadel.settings.ExternalDomain; 18 18 stateDir = appsDir; 19 19 inherit patFile; 20 - inherit (cfg) loginPolicy idps; 20 + inherit (cfg) loginPolicy idps smtp; 21 21 projects = lib.mapAttrs (_: project: { 22 22 inherit (project) access users; 23 23 apps = lib.mapAttrs (_: app: { ··· 39 39 description = '' 40 40 Fields of the instance's default login policy, as in Zitadel's admin 41 41 API (PUT /admin/v1/policies/login). Unlisted fields keep their value. 42 + ''; 43 + }; 44 + 45 + smtp = lib.mkOption { 46 + type = lib.types.nullOr (lib.types.attrsOf lib.types.anything); 47 + default = null; 48 + example = { 49 + senderAddress = "id@example.org"; 50 + senderName = "example.org ID"; 51 + host = "mail.example.org:465"; 52 + tls = true; 53 + user = "id@example.org"; 54 + passwordFile = "/run/secrets/id-mail-password"; 55 + }; 56 + description = '' 57 + SMTP provider for the instance's emails (verification codes, 58 + password resets), as in Zitadel's admin API (POST /admin/v1/smtp), 59 + with the password read from passwordFile (Zitadel ignores providers 60 + without one). It is created when missing, updated when it differs 61 + and activated. Without one, users whose email address is not 62 + verified cannot finish logging in. 42 63 ''; 43 64 }; 44 65
+42
modules/zitadel/reconcile.py
··· 2 2 # admin and management APIs. Objects are found by name, created when missing 3 3 # and updated when they differ. Projects and apps missing from the config 4 4 # are left alone. 5 + import hashlib 5 6 import json 6 7 import os 7 8 import pwd ··· 75 76 return any( 76 77 current.get(k, ZERO.get(k, False)) != v for k, v in wanted.items() 77 78 ) 79 + 80 + 81 + # SMTP provider (instance), found by its description 82 + 83 + if config["smtp"] is not None: 84 + wanted = {"description": "lass.zitadel", **config["smtp"]} 85 + with open(wanted.pop("passwordFile")) as f: 86 + password = f.read().strip() 87 + # Zitadel never shows the password, so a hash of the one it has is kept 88 + password_hash = hashlib.sha256(password.encode()).hexdigest() 89 + state_file = f"{config['stateDir']}/smtp.json" 90 + saved = {} 91 + if os.path.exists(state_file): 92 + with open(state_file) as f: 93 + saved = json.load(f) 94 + current = next(( 95 + c for c in search("/admin/v1/smtp/_search") 96 + if c.get("description") == wanted["description"] 97 + ), None) 98 + if current is None: 99 + smtp_id = call("POST", "/admin/v1/smtp", { 100 + **wanted, 101 + "password": password, 102 + })["id"] 103 + print("smtp: created") 104 + else: 105 + smtp_id = current["id"] 106 + if differs(current, wanted): 107 + call("PUT", f"/admin/v1/smtp/{smtp_id}", wanted) 108 + print("smtp: updated") 109 + if saved.get(smtp_id) != password_hash: 110 + call("PUT", f"/admin/v1/smtp/{smtp_id}/password", { 111 + "password": password, 112 + }) 113 + print("smtp: new password") 114 + if saved.get(smtp_id) != password_hash: 115 + with open(state_file, "w") as f: 116 + json.dump({smtp_id: password_hash}, f) 117 + if current is None or current.get("state") != "SMTP_CONFIG_ACTIVE": 118 + call("POST", f"/admin/v1/smtp/{smtp_id}/_activate", {}) 119 + print("smtp: activated") 78 120 79 121 80 122 # login policy and external IdPs (instance defaults)