This repository has no description
9.4 kB
265 lines
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.lass.zitadel;
9 zitadel = config.services.zitadel;
10 appsDir = "/var/lib/zitadel-apps";
11 # Written by zitadel's setup when it creates the first instance.
12 patFile = "/var/lib/zitadel/reconcile.pat";
13
14 reconcileConfig = pkgs.writeText "zitadel-reconcile.json" (
15 builtins.toJSON {
16 url = "http://127.0.0.1:${toString zitadel.settings.Port}";
17 domain = zitadel.settings.ExternalDomain;
18 stateDir = appsDir;
19 inherit patFile;
20 inherit (cfg) loginPolicy idps smtp;
21 projects = lib.mapAttrs (_: project: {
22 inherit (project) access users;
23 apps = lib.mapAttrs (_: app: {
24 inherit (app) redirectUris envFile;
25 authMethodType = "OIDC_AUTH_METHOD_TYPE_${lib.toUpper app.authMethod}";
26 }) project.apps;
27 }) cfg.projects;
28 }
29 );
30in
31{
32 options.lass.zitadel = {
33 loginPolicy = lib.mkOption {
34 type = lib.types.attrsOf lib.types.anything;
35 default = { };
36 example = {
37 allowRegister = true;
38 };
39 description = ''
40 Fields of the instance's default login policy, as in Zitadel's admin
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.
63 '';
64 };
65
66 idps = lib.mkOption {
67 default = { };
68 description = ''
69 External identity providers, added to the login policy. An IdP whose
70 client id or secret is empty is skipped.
71 '';
72 type = lib.types.attrsOf (
73 lib.types.submodule (
74 { name, ... }:
75 {
76 options = {
77 type = lib.mkOption {
78 type = lib.types.enum [
79 "github"
80 "google"
81 "oidc"
82 ];
83 };
84 name = lib.mkOption {
85 type = lib.types.str;
86 default = name;
87 description = "Display name on the login page; the IdP is found by it.";
88 };
89 issuer = lib.mkOption {
90 type = lib.types.nullOr lib.types.str;
91 default = null;
92 description = "Issuer of a generic OIDC IdP (type = \"oidc\").";
93 };
94 clientId = lib.mkOption {
95 type = lib.types.nullOr lib.types.str;
96 default = null;
97 };
98 clientIdFile = lib.mkOption {
99 type = lib.types.nullOr lib.types.str;
100 default = null;
101 };
102 clientSecretFile = lib.mkOption { type = lib.types.str; };
103 scopes = lib.mkOption {
104 type = lib.types.listOf lib.types.str;
105 default = [
106 "openid"
107 "profile"
108 "email"
109 ];
110 };
111 providerOptions = lib.mkOption {
112 type = lib.types.attrsOf lib.types.bool;
113 default = {
114 isLinkingAllowed = true;
115 isCreationAllowed = true;
116 isAutoUpdate = true;
117 };
118 description = ''
119 Options of the IdP, as in Zitadel's admin API
120 (zitadel.idp.v1.Options).
121 '';
122 };
123 };
124 }
125 )
126 );
127 };
128
129 projects = lib.mkOption {
130 default = { };
131 description = ''
132 Zitadel projects, one per service, in the default organization.
133 Projects and apps removed from here are left in Zitadel.
134 '';
135 type = lib.types.attrsOf (
136 lib.types.submodule (
137 { name, ... }:
138 let
139 project = name;
140 in
141 {
142 options = {
143 access = lib.mkOption {
144 type = lib.types.enum [
145 "public"
146 "declarative"
147 "managed"
148 ];
149 description = ''
150 Who may log in to the project's apps:
151 - public: every user of the instance.
152 - declarative: the users listed in `users`. All other user
153 grants of the project are removed.
154 - managed: users with a user grant on the project, given by
155 an admin in the console.
156 '';
157 };
158 users = lib.mkOption {
159 type = lib.types.listOf lib.types.str;
160 default = [ ];
161 description = ''
162 Verified email addresses of the users that get a user
163 grant on the project (access = "declarative"). Not login
164 names: anyone can register a free one that looks like
165 someone's address. Users who have not registered or
166 verified their address yet are skipped until the next run.
167 '';
168 };
169 apps = lib.mkOption {
170 default = { };
171 description = "OIDC web apps of the project.";
172 type = lib.types.attrsOf (
173 lib.types.submodule (
174 { name, ... }:
175 {
176 options = {
177 redirectUris = lib.mkOption { type = lib.types.listOf lib.types.str; };
178 authMethod = lib.mkOption {
179 type = lib.types.enum [
180 "basic"
181 "post"
182 ];
183 default = "basic";
184 description = "How the app sends its client secret to the token endpoint.";
185 };
186 envFile = {
187 path = lib.mkOption {
188 type = lib.types.str;
189 default = "${appsDir}/${project}-${name}.env";
190 description = "Env file with the app's client id and secret.";
191 };
192 owner = lib.mkOption {
193 type = lib.types.str;
194 default = "root";
195 };
196 clientIdVariable = lib.mkOption {
197 type = lib.types.str;
198 default = "CLIENT_ID";
199 };
200 clientSecretVariable = lib.mkOption {
201 type = lib.types.str;
202 default = "CLIENT_SECRET";
203 };
204 };
205 };
206 }
207 )
208 );
209 };
210 };
211 }
212 )
213 );
214 };
215 };
216
217 config = lib.mkIf zitadel.enable {
218 assertions =
219 lib.mapAttrsToList (name: project: {
220 assertion = project.users == [ ] || project.access == "declarative";
221 message = "lass.zitadel.projects.${name}.users needs access = \"declarative\"";
222 }) cfg.projects
223 ++ lib.mapAttrsToList (name: idp: {
224 assertion = (idp.clientId == null) != (idp.clientIdFile == null);
225 message = "lass.zitadel.idps.${name} needs exactly one of clientId and clientIdFile";
226 }) cfg.idps
227 ++ lib.mapAttrsToList (name: idp: {
228 assertion = (idp.type == "oidc") == (idp.issuer != null);
229 message = "lass.zitadel.idps.${name}.issuer is needed for, and only for, type = \"oidc\"";
230 }) cfg.idps;
231
232 services.zitadel.steps.FirstInstance = {
233 PatPath = patFile;
234 Org.Machine.Machine = {
235 Username = "reconcile";
236 Name = "zitadel-reconcile";
237 };
238 # Zitadel's default for tokens without expiry; an empty Pat section
239 # creates no token.
240 Org.Machine.Pat.ExpirationDate = "9999-01-01T00:00:00Z";
241 };
242 systemd.services.zitadel.serviceConfig = {
243 StateDirectory = "zitadel";
244 StateDirectoryMode = "0700";
245 };
246
247 systemd.services.zitadel-reconcile = {
248 description = "Apply lass.zitadel to Zitadel";
249 wantedBy = [ "multi-user.target" ];
250 wants = [ "zitadel.service" ];
251 after = [ "zitadel.service" ];
252 environment.PYTHONUNBUFFERED = "1";
253 serviceConfig = {
254 Type = "oneshot";
255 RemainAfterExit = true;
256 ExecStart = "${
257 pkgs.writers.writePython3 "zitadel-reconcile" { } ./reconcile.py
258 } ${reconcileConfig}";
259 StateDirectory = "zitadel-apps";
260 StateDirectoryMode = "0711";
261 UMask = "0077";
262 };
263 };
264 };
265}