This repository has no description
8.5 kB
244 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 Verified email addresses of the users that get a user
142 grant on the project (access = "declarative"). Not login
143 names: anyone can register a free one that looks like
144 someone's address. Users who have not registered or
145 verified their address yet are skipped until the next run.
146 '';
147 };
148 apps = lib.mkOption {
149 default = { };
150 description = "OIDC web apps of the project.";
151 type = lib.types.attrsOf (
152 lib.types.submodule (
153 { name, ... }:
154 {
155 options = {
156 redirectUris = lib.mkOption { type = lib.types.listOf lib.types.str; };
157 authMethod = lib.mkOption {
158 type = lib.types.enum [
159 "basic"
160 "post"
161 ];
162 default = "basic";
163 description = "How the app sends its client secret to the token endpoint.";
164 };
165 envFile = {
166 path = lib.mkOption {
167 type = lib.types.str;
168 default = "${appsDir}/${project}-${name}.env";
169 description = "Env file with the app's client id and secret.";
170 };
171 owner = lib.mkOption {
172 type = lib.types.str;
173 default = "root";
174 };
175 clientIdVariable = lib.mkOption {
176 type = lib.types.str;
177 default = "CLIENT_ID";
178 };
179 clientSecretVariable = lib.mkOption {
180 type = lib.types.str;
181 default = "CLIENT_SECRET";
182 };
183 };
184 };
185 }
186 )
187 );
188 };
189 };
190 }
191 )
192 );
193 };
194 };
195
196 config = lib.mkIf zitadel.enable {
197 assertions =
198 lib.mapAttrsToList (name: project: {
199 assertion = project.users == [ ] || project.access == "declarative";
200 message = "lass.zitadel.projects.${name}.users needs access = \"declarative\"";
201 }) cfg.projects
202 ++ lib.mapAttrsToList (name: idp: {
203 assertion = (idp.clientId == null) != (idp.clientIdFile == null);
204 message = "lass.zitadel.idps.${name} needs exactly one of clientId and clientIdFile";
205 }) cfg.idps
206 ++ lib.mapAttrsToList (name: idp: {
207 assertion = (idp.type == "oidc") == (idp.issuer != null);
208 message = "lass.zitadel.idps.${name}.issuer is needed for, and only for, type = \"oidc\"";
209 }) cfg.idps;
210
211 services.zitadel.steps.FirstInstance = {
212 PatPath = patFile;
213 Org.Machine.Machine = {
214 Username = "reconcile";
215 Name = "zitadel-reconcile";
216 };
217 # Zitadel's default for tokens without expiry; an empty Pat section
218 # creates no token.
219 Org.Machine.Pat.ExpirationDate = "9999-01-01T00:00:00Z";
220 };
221 systemd.services.zitadel.serviceConfig = {
222 StateDirectory = "zitadel";
223 StateDirectoryMode = "0700";
224 };
225
226 systemd.services.zitadel-reconcile = {
227 description = "Apply lass.zitadel to Zitadel";
228 wantedBy = [ "multi-user.target" ];
229 wants = [ "zitadel.service" ];
230 after = [ "zitadel.service" ];
231 environment.PYTHONUNBUFFERED = "1";
232 serviceConfig = {
233 Type = "oneshot";
234 RemainAfterExit = true;
235 ExecStart = "${
236 pkgs.writers.writePython3 "zitadel-reconcile" { } ./reconcile.py
237 } ${reconcileConfig}";
238 StateDirectory = "zitadel-apps";
239 StateDirectoryMode = "0711";
240 UMask = "0077";
241 };
242 };
243 };
244}