forked from
lass.pds.lassul.us/superconfig
This repository has no description
10 kB
278 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 grantTypes = [
27 "OIDC_GRANT_TYPE_AUTHORIZATION_CODE"
28 ]
29 ++ lib.optional app.refreshTokens "OIDC_GRANT_TYPE_REFRESH_TOKEN";
30 }) project.apps;
31 }) cfg.projects;
32 }
33 );
34in
35{
36 options.lass.zitadel = {
37 loginPolicy = lib.mkOption {
38 type = lib.types.attrsOf lib.types.anything;
39 default = { };
40 example = {
41 allowRegister = true;
42 };
43 description = ''
44 Fields of the instance's default login policy, as in Zitadel's admin
45 API (PUT /admin/v1/policies/login). Unlisted fields keep their value.
46 '';
47 };
48
49 smtp = lib.mkOption {
50 type = lib.types.nullOr (lib.types.attrsOf lib.types.anything);
51 default = null;
52 example = {
53 senderAddress = "id@example.org";
54 senderName = "example.org ID";
55 host = "mail.example.org:465";
56 tls = true;
57 user = "id@example.org";
58 passwordFile = "/run/secrets/id-mail-password";
59 };
60 description = ''
61 SMTP provider for the instance's emails (verification codes,
62 password resets), as in Zitadel's admin API (POST /admin/v1/smtp),
63 with the password read from passwordFile (Zitadel ignores providers
64 without one). It is created when missing, updated when it differs
65 and activated. Without one, users whose email address is not
66 verified cannot finish logging in.
67 '';
68 };
69
70 idps = lib.mkOption {
71 default = { };
72 description = ''
73 External identity providers, added to the login policy. An IdP whose
74 client id or secret is empty is skipped.
75 '';
76 type = lib.types.attrsOf (
77 lib.types.submodule (
78 { name, ... }:
79 {
80 options = {
81 type = lib.mkOption {
82 type = lib.types.enum [
83 "github"
84 "google"
85 "oidc"
86 ];
87 };
88 name = lib.mkOption {
89 type = lib.types.str;
90 default = name;
91 description = "Display name on the login page; the IdP is found by it.";
92 };
93 issuer = lib.mkOption {
94 type = lib.types.nullOr lib.types.str;
95 default = null;
96 description = "Issuer of a generic OIDC IdP (type = \"oidc\").";
97 };
98 clientId = lib.mkOption {
99 type = lib.types.nullOr lib.types.str;
100 default = null;
101 };
102 clientIdFile = lib.mkOption {
103 type = lib.types.nullOr lib.types.str;
104 default = null;
105 };
106 clientSecretFile = lib.mkOption { type = lib.types.str; };
107 scopes = lib.mkOption {
108 type = lib.types.listOf lib.types.str;
109 default = [
110 "openid"
111 "profile"
112 "email"
113 ];
114 };
115 providerOptions = lib.mkOption {
116 type = lib.types.attrsOf lib.types.bool;
117 default = {
118 isLinkingAllowed = true;
119 isCreationAllowed = true;
120 isAutoUpdate = true;
121 };
122 description = ''
123 Options of the IdP, as in Zitadel's admin API
124 (zitadel.idp.v1.Options).
125 '';
126 };
127 };
128 }
129 )
130 );
131 };
132
133 projects = lib.mkOption {
134 default = { };
135 description = ''
136 Zitadel projects, one per service, in the default organization.
137 Projects and apps removed from here are left in Zitadel.
138 '';
139 type = lib.types.attrsOf (
140 lib.types.submodule (
141 { name, ... }:
142 let
143 project = name;
144 in
145 {
146 options = {
147 access = lib.mkOption {
148 type = lib.types.enum [
149 "public"
150 "declarative"
151 "managed"
152 ];
153 description = ''
154 Who may log in to the project's apps:
155 - public: every user of the instance.
156 - declarative: the users listed in `users`. All other user
157 grants of the project are removed.
158 - managed: users with a user grant on the project, given by
159 an admin in the console.
160 '';
161 };
162 users = lib.mkOption {
163 type = lib.types.listOf lib.types.str;
164 default = [ ];
165 description = ''
166 Verified email addresses of the users that get a user
167 grant on the project (access = "declarative"). Not login
168 names: anyone can register a free one that looks like
169 someone's address. Users who have not registered or
170 verified their address yet are skipped until the next run.
171 '';
172 };
173 apps = lib.mkOption {
174 default = { };
175 description = "OIDC web apps of the project.";
176 type = lib.types.attrsOf (
177 lib.types.submodule (
178 { name, ... }:
179 {
180 options = {
181 redirectUris = lib.mkOption { type = lib.types.listOf lib.types.str; };
182 authMethod = lib.mkOption {
183 type = lib.types.enum [
184 "basic"
185 "post"
186 ];
187 default = "basic";
188 description = "How the app sends its client secret to the token endpoint.";
189 };
190 refreshTokens = lib.mkOption {
191 type = lib.types.bool;
192 default = false;
193 description = ''
194 Allow the refresh_token grant. Zitadel hands out
195 refresh tokens only when the app requests the
196 offline_access scope.
197 '';
198 };
199 envFile = {
200 path = lib.mkOption {
201 type = lib.types.str;
202 default = "${appsDir}/${project}-${name}.env";
203 description = "Env file with the app's client id and secret.";
204 };
205 owner = lib.mkOption {
206 type = lib.types.str;
207 default = "root";
208 };
209 clientIdVariable = lib.mkOption {
210 type = lib.types.str;
211 default = "CLIENT_ID";
212 };
213 clientSecretVariable = lib.mkOption {
214 type = lib.types.str;
215 default = "CLIENT_SECRET";
216 };
217 };
218 };
219 }
220 )
221 );
222 };
223 };
224 }
225 )
226 );
227 };
228 };
229
230 config = lib.mkIf zitadel.enable {
231 assertions =
232 lib.mapAttrsToList (name: project: {
233 assertion = project.users == [ ] || project.access == "declarative";
234 message = "lass.zitadel.projects.${name}.users needs access = \"declarative\"";
235 }) cfg.projects
236 ++ lib.mapAttrsToList (name: idp: {
237 assertion = (idp.clientId == null) != (idp.clientIdFile == null);
238 message = "lass.zitadel.idps.${name} needs exactly one of clientId and clientIdFile";
239 }) cfg.idps
240 ++ lib.mapAttrsToList (name: idp: {
241 assertion = (idp.type == "oidc") == (idp.issuer != null);
242 message = "lass.zitadel.idps.${name}.issuer is needed for, and only for, type = \"oidc\"";
243 }) cfg.idps;
244
245 services.zitadel.steps.FirstInstance = {
246 PatPath = patFile;
247 Org.Machine.Machine = {
248 Username = "reconcile";
249 Name = "zitadel-reconcile";
250 };
251 # Zitadel's default for tokens without expiry; an empty Pat section
252 # creates no token.
253 Org.Machine.Pat.ExpirationDate = "9999-01-01T00:00:00Z";
254 };
255 systemd.services.zitadel.serviceConfig = {
256 StateDirectory = "zitadel";
257 StateDirectoryMode = "0700";
258 };
259
260 systemd.services.zitadel-reconcile = {
261 description = "Apply lass.zitadel to Zitadel";
262 wantedBy = [ "multi-user.target" ];
263 wants = [ "zitadel.service" ];
264 after = [ "zitadel.service" ];
265 environment.PYTHONUNBUFFERED = "1";
266 serviceConfig = {
267 Type = "oneshot";
268 RemainAfterExit = true;
269 ExecStart = "${
270 pkgs.writers.writePython3 "zitadel-reconcile" { } ./reconcile.py
271 } ${reconcileConfig}";
272 StateDirectory = "zitadel-apps";
273 StateDirectoryMode = "0711";
274 UMask = "0077";
275 };
276 };
277 };
278}