This repository has no description
10 kB
283 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 "none"
187 ];
188 default = "basic";
189 description = ''
190 How the app sends its client secret to the token
191 endpoint; "none" for a public client without a
192 secret, which has to use PKCE.
193 '';
194 };
195 refreshTokens = lib.mkOption {
196 type = lib.types.bool;
197 default = false;
198 description = ''
199 Allow the refresh_token grant. Zitadel hands out
200 refresh tokens only when the app requests the
201 offline_access scope.
202 '';
203 };
204 envFile = {
205 path = lib.mkOption {
206 type = lib.types.str;
207 default = "${appsDir}/${project}-${name}.env";
208 description = "Env file with the app's client id and secret (if any).";
209 };
210 owner = lib.mkOption {
211 type = lib.types.str;
212 default = "root";
213 };
214 clientIdVariable = lib.mkOption {
215 type = lib.types.str;
216 default = "CLIENT_ID";
217 };
218 clientSecretVariable = lib.mkOption {
219 type = lib.types.str;
220 default = "CLIENT_SECRET";
221 };
222 };
223 };
224 }
225 )
226 );
227 };
228 };
229 }
230 )
231 );
232 };
233 };
234
235 config = lib.mkIf zitadel.enable {
236 assertions =
237 lib.mapAttrsToList (name: project: {
238 assertion = project.users == [ ] || project.access == "declarative";
239 message = "lass.zitadel.projects.${name}.users needs access = \"declarative\"";
240 }) cfg.projects
241 ++ lib.mapAttrsToList (name: idp: {
242 assertion = (idp.clientId == null) != (idp.clientIdFile == null);
243 message = "lass.zitadel.idps.${name} needs exactly one of clientId and clientIdFile";
244 }) cfg.idps
245 ++ lib.mapAttrsToList (name: idp: {
246 assertion = (idp.type == "oidc") == (idp.issuer != null);
247 message = "lass.zitadel.idps.${name}.issuer is needed for, and only for, type = \"oidc\"";
248 }) cfg.idps;
249
250 services.zitadel.steps.FirstInstance = {
251 PatPath = patFile;
252 Org.Machine.Machine = {
253 Username = "reconcile";
254 Name = "zitadel-reconcile";
255 };
256 # Zitadel's default for tokens without expiry; an empty Pat section
257 # creates no token.
258 Org.Machine.Pat.ExpirationDate = "9999-01-01T00:00:00Z";
259 };
260 systemd.services.zitadel.serviceConfig = {
261 StateDirectory = "zitadel";
262 StateDirectoryMode = "0700";
263 };
264
265 systemd.services.zitadel-reconcile = {
266 description = "Apply lass.zitadel to Zitadel";
267 wantedBy = [ "multi-user.target" ];
268 wants = [ "zitadel.service" ];
269 after = [ "zitadel.service" ];
270 environment.PYTHONUNBUFFERED = "1";
271 serviceConfig = {
272 Type = "oneshot";
273 RemainAfterExit = true;
274 ExecStart = "${
275 pkgs.writers.writePython3 "zitadel-reconcile" { } ./reconcile.py
276 } ${reconcileConfig}";
277 StateDirectory = "zitadel-apps";
278 StateDirectoryMode = "0711";
279 UMask = "0077";
280 };
281 };
282 };
283}