This repository has no description
8.4 kB
243 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;
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 idps = lib.mkOption {
46 default = { };
47 description = ''
48 External identity providers, added to the login policy. An IdP whose
49 client id or secret is empty is skipped.
50 '';
51 type = lib.types.attrsOf (
52 lib.types.submodule (
53 { name, ... }:
54 {
55 options = {
56 type = lib.mkOption {
57 type = lib.types.enum [
58 "github"
59 "google"
60 "oidc"
61 ];
62 };
63 name = lib.mkOption {
64 type = lib.types.str;
65 default = name;
66 description = "Display name on the login page; the IdP is found by it.";
67 };
68 issuer = lib.mkOption {
69 type = lib.types.nullOr lib.types.str;
70 default = null;
71 description = "Issuer of a generic OIDC IdP (type = \"oidc\").";
72 };
73 clientId = lib.mkOption {
74 type = lib.types.nullOr lib.types.str;
75 default = null;
76 };
77 clientIdFile = lib.mkOption {
78 type = lib.types.nullOr lib.types.str;
79 default = null;
80 };
81 clientSecretFile = lib.mkOption { type = lib.types.str; };
82 scopes = lib.mkOption {
83 type = lib.types.listOf lib.types.str;
84 default = [
85 "openid"
86 "profile"
87 "email"
88 ];
89 };
90 providerOptions = lib.mkOption {
91 type = lib.types.attrsOf lib.types.bool;
92 default = {
93 isLinkingAllowed = true;
94 isCreationAllowed = true;
95 isAutoUpdate = true;
96 };
97 description = ''
98 Options of the IdP, as in Zitadel's admin API
99 (zitadel.idp.v1.Options).
100 '';
101 };
102 };
103 }
104 )
105 );
106 };
107
108 projects = lib.mkOption {
109 default = { };
110 description = ''
111 Zitadel projects, one per service, in the default organization.
112 Projects and apps removed from here are left in Zitadel.
113 '';
114 type = lib.types.attrsOf (
115 lib.types.submodule (
116 { name, ... }:
117 let
118 project = name;
119 in
120 {
121 options = {
122 access = lib.mkOption {
123 type = lib.types.enum [
124 "public"
125 "declarative"
126 "managed"
127 ];
128 description = ''
129 Who may log in to the project's apps:
130 - public: every user of the instance.
131 - declarative: the users listed in `users`. All other user
132 grants of the project are removed.
133 - managed: users with a user grant on the project, given by
134 an admin in the console.
135 '';
136 };
137 users = lib.mkOption {
138 type = lib.types.listOf lib.types.str;
139 default = [ ];
140 description = ''
141 Login names or verified email addresses of the users that
142 get a user grant on the project (access = "declarative").
143 Users who have not registered yet are skipped until the
144 next run.
145 '';
146 };
147 apps = lib.mkOption {
148 default = { };
149 description = "OIDC web apps of the project.";
150 type = lib.types.attrsOf (
151 lib.types.submodule (
152 { name, ... }:
153 {
154 options = {
155 redirectUris = lib.mkOption { type = lib.types.listOf lib.types.str; };
156 authMethod = lib.mkOption {
157 type = lib.types.enum [
158 "basic"
159 "post"
160 ];
161 default = "basic";
162 description = "How the app sends its client secret to the token endpoint.";
163 };
164 envFile = {
165 path = lib.mkOption {
166 type = lib.types.str;
167 default = "${appsDir}/${project}-${name}.env";
168 description = "Env file with the app's client id and secret.";
169 };
170 owner = lib.mkOption {
171 type = lib.types.str;
172 default = "root";
173 };
174 clientIdVariable = lib.mkOption {
175 type = lib.types.str;
176 default = "CLIENT_ID";
177 };
178 clientSecretVariable = lib.mkOption {
179 type = lib.types.str;
180 default = "CLIENT_SECRET";
181 };
182 };
183 };
184 }
185 )
186 );
187 };
188 };
189 }
190 )
191 );
192 };
193 };
194
195 config = lib.mkIf zitadel.enable {
196 assertions =
197 lib.mapAttrsToList (name: project: {
198 assertion = project.users == [ ] || project.access == "declarative";
199 message = "lass.zitadel.projects.${name}.users needs access = \"declarative\"";
200 }) cfg.projects
201 ++ lib.mapAttrsToList (name: idp: {
202 assertion = (idp.clientId == null) != (idp.clientIdFile == null);
203 message = "lass.zitadel.idps.${name} needs exactly one of clientId and clientIdFile";
204 }) cfg.idps
205 ++ lib.mapAttrsToList (name: idp: {
206 assertion = (idp.type == "oidc") == (idp.issuer != null);
207 message = "lass.zitadel.idps.${name}.issuer is needed for, and only for, type = \"oidc\"";
208 }) cfg.idps;
209
210 services.zitadel.steps.FirstInstance = {
211 PatPath = patFile;
212 Org.Machine.Machine = {
213 Username = "reconcile";
214 Name = "zitadel-reconcile";
215 };
216 # Zitadel's default for tokens without expiry; an empty Pat section
217 # creates no token.
218 Org.Machine.Pat.ExpirationDate = "9999-01-01T00:00:00Z";
219 };
220 systemd.services.zitadel.serviceConfig = {
221 StateDirectory = "zitadel";
222 StateDirectoryMode = "0700";
223 };
224
225 systemd.services.zitadel-reconcile = {
226 description = "Apply lass.zitadel to Zitadel";
227 wantedBy = [ "multi-user.target" ];
228 wants = [ "zitadel.service" ];
229 after = [ "zitadel.service" ];
230 environment.PYTHONUNBUFFERED = "1";
231 serviceConfig = {
232 Type = "oneshot";
233 RemainAfterExit = true;
234 ExecStart = "${
235 pkgs.writers.writePython3 "zitadel-reconcile" { } ./reconcile.py
236 } ${reconcileConfig}";
237 StateDirectory = "zitadel-apps";
238 StateDirectoryMode = "0711";
239 UMask = "0077";
240 };
241 };
242 };
243}