1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 
3 #include "errno-util.h"
4 #include "fd-util.h"
5 #include "fileio.h"
6 #include "format-util.h"
7 #include "path-util.h"
8 #include "stdio-util.h"
9 #include "user-util.h"
10 #include "userdb-dropin.h"
11 
load_user(FILE * f,const char * path,const char * name,uid_t uid,UserDBFlags flags,UserRecord ** ret)12 static int load_user(
13                 FILE *f,
14                 const char *path,
15                 const char *name,
16                 uid_t uid,
17                 UserDBFlags flags,
18                 UserRecord **ret) {
19 
20         _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
21         _cleanup_(user_record_unrefp) UserRecord *u = NULL;
22         bool have_privileged;
23         int r;
24 
25         assert(f);
26 
27         r = json_parse_file(f, path, 0, &v, NULL, NULL);
28         if (r < 0)
29                 return r;
30 
31         if (FLAGS_SET(flags, USERDB_SUPPRESS_SHADOW) || !path || !(name || uid_is_valid(uid)))
32                 have_privileged = false;
33         else {
34                 _cleanup_(json_variant_unrefp) JsonVariant *privileged_v = NULL;
35                 _cleanup_free_ char *d = NULL, *j = NULL;
36 
37                 /* Let's load the "privileged" section from a companion file. But only if USERDB_AVOID_SHADOW
38                  * is not set. After all, the privileged section kinda takes the role of the data from the
39                  * shadow file, hence it makes sense to use the same flag here.
40                  *
41                  * The general assumption is that whoever provides these records makes the .user file
42                  * world-readable, but the .privilege file readable to root and the assigned UID only. But we
43                  * won't verify that here, as it would be too late. */
44 
45                 r = path_extract_directory(path, &d);
46                 if (r < 0)
47                         return r;
48 
49                 if (name) {
50                         j = strjoin(d, "/", name, ".user-privileged");
51                         if (!j)
52                                 return -ENOMEM;
53                 } else {
54                         assert(uid_is_valid(uid));
55                         if (asprintf(&j, "%s/" UID_FMT ".user-privileged", d, uid) < 0)
56                                 return -ENOMEM;
57                 }
58 
59                 r = json_parse_file(NULL, j, JSON_PARSE_SENSITIVE, &privileged_v, NULL, NULL);
60                 if (ERRNO_IS_PRIVILEGE(r))
61                         have_privileged = false;
62                 else if (r == -ENOENT)
63                         have_privileged = true; /* if the privileged file doesn't exist, we are complete */
64                 else if (r < 0)
65                         return r;
66                 else {
67                         r = json_variant_merge(&v, privileged_v);
68                         if (r < 0)
69                                 return r;
70 
71                         have_privileged = true;
72                 }
73         }
74 
75         u = user_record_new();
76         if (!u)
77                 return -ENOMEM;
78 
79         r = user_record_load(
80                         u, v,
81                         USER_RECORD_REQUIRE_REGULAR|
82                         USER_RECORD_ALLOW_PER_MACHINE|
83                         USER_RECORD_ALLOW_BINDING|
84                         USER_RECORD_ALLOW_SIGNATURE|
85                         (have_privileged ? USER_RECORD_ALLOW_PRIVILEGED : 0)|
86                         USER_RECORD_PERMISSIVE);
87         if (r < 0)
88                 return r;
89 
90         if (name && !streq_ptr(name, u->user_name))
91                 return -EINVAL;
92 
93         if (uid_is_valid(uid) && uid != u->uid)
94                 return -EINVAL;
95 
96         u->incomplete = !have_privileged;
97 
98         if (ret)
99                 *ret = TAKE_PTR(u);
100 
101         return 0;
102 }
103 
dropin_user_record_by_name(const char * name,const char * path,UserDBFlags flags,UserRecord ** ret)104 int dropin_user_record_by_name(const char *name, const char *path, UserDBFlags flags, UserRecord **ret) {
105         _cleanup_free_ char *found_path = NULL;
106         _cleanup_fclose_ FILE *f = NULL;
107         int r;
108 
109         assert(name);
110 
111         if (path) {
112                 f = fopen(path, "re");
113                 if (!f)
114                         return errno == ENOENT ? -ESRCH : -errno; /* We generally want ESRCH to indicate no such user */
115         } else {
116                 const char *j;
117 
118                 j = strjoina(name, ".user");
119                 if (!filename_is_valid(j)) /* Doesn't qualify as valid filename? Then it's definitely not provided as a drop-in */
120                         return -ESRCH;
121 
122                 r = search_and_fopen_nulstr(j, "re", NULL, USERDB_DROPIN_DIR_NULSTR("userdb"), &f, &found_path);
123                 if (r == -ENOENT)
124                         return -ESRCH;
125                 if (r < 0)
126                         return r;
127 
128                 path = found_path;
129         }
130 
131         return load_user(f, path, name, UID_INVALID, flags, ret);
132 }
133 
dropin_user_record_by_uid(uid_t uid,const char * path,UserDBFlags flags,UserRecord ** ret)134 int dropin_user_record_by_uid(uid_t uid, const char *path, UserDBFlags flags, UserRecord **ret) {
135         _cleanup_free_ char *found_path = NULL;
136         _cleanup_fclose_ FILE *f = NULL;
137         int r;
138 
139         assert(uid_is_valid(uid));
140 
141         if (path) {
142                 f = fopen(path, "re");
143                 if (!f)
144                         return errno == ENOENT ? -ESRCH : -errno;
145         } else {
146                 char buf[DECIMAL_STR_MAX(uid_t) + STRLEN(".user") + 1];
147 
148                 xsprintf(buf, UID_FMT ".user", uid);
149                 /* Note that we don't bother to validate this as a filename, as this is generated from a decimal
150                  * integer, i.e. is definitely OK as a filename */
151 
152                 r = search_and_fopen_nulstr(buf, "re", NULL, USERDB_DROPIN_DIR_NULSTR("userdb"), &f, &found_path);
153                 if (r == -ENOENT)
154                         return -ESRCH;
155                 if (r < 0)
156                         return r;
157 
158                 path = found_path;
159         }
160 
161         return load_user(f, path, NULL, uid, flags, ret);
162 }
163 
load_group(FILE * f,const char * path,const char * name,gid_t gid,UserDBFlags flags,GroupRecord ** ret)164 static int load_group(
165                 FILE *f,
166                 const char *path,
167                 const char *name,
168                 gid_t gid,
169                 UserDBFlags flags,
170                 GroupRecord **ret) {
171 
172         _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
173         _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
174         bool have_privileged;
175         int r;
176 
177         assert(f);
178 
179         r = json_parse_file(f, path, 0, &v, NULL, NULL);
180         if (r < 0)
181                 return r;
182 
183         if (FLAGS_SET(flags, USERDB_SUPPRESS_SHADOW) || !path || !(name || gid_is_valid(gid)))
184                 have_privileged = false;
185         else {
186                 _cleanup_(json_variant_unrefp) JsonVariant *privileged_v = NULL;
187                 _cleanup_free_ char *d = NULL, *j = NULL;
188 
189                 r = path_extract_directory(path, &d);
190                 if (r < 0)
191                         return r;
192 
193                 if (name) {
194                         j = strjoin(d, "/", name, ".group-privileged");
195                         if (!j)
196                                 return -ENOMEM;
197                 } else {
198                         assert(gid_is_valid(gid));
199                         if (asprintf(&j, "%s/" GID_FMT ".group-privileged", d, gid) < 0)
200                                 return -ENOMEM;
201                 }
202 
203                 r = json_parse_file(NULL, j, JSON_PARSE_SENSITIVE, &privileged_v, NULL, NULL);
204                 if (ERRNO_IS_PRIVILEGE(r))
205                         have_privileged = false;
206                 else if (r == -ENOENT)
207                         have_privileged = true; /* if the privileged file doesn't exist, we are complete */
208                 else if (r < 0)
209                         return r;
210                 else {
211                         r = json_variant_merge(&v, privileged_v);
212                         if (r < 0)
213                                 return r;
214 
215                         have_privileged = true;
216                 }
217         }
218 
219         g = group_record_new();
220         if (!g)
221                 return -ENOMEM;
222 
223         r = group_record_load(
224                         g, v,
225                         USER_RECORD_REQUIRE_REGULAR|
226                         USER_RECORD_ALLOW_PER_MACHINE|
227                         USER_RECORD_ALLOW_BINDING|
228                         USER_RECORD_ALLOW_SIGNATURE|
229                         (have_privileged ? USER_RECORD_ALLOW_PRIVILEGED : 0)|
230                         USER_RECORD_PERMISSIVE);
231         if (r < 0)
232                 return r;
233 
234         if (name && !streq_ptr(name, g->group_name))
235                 return -EINVAL;
236 
237         if (gid_is_valid(gid) && gid != g->gid)
238                 return -EINVAL;
239 
240         g->incomplete = !have_privileged;
241 
242         if (ret)
243                 *ret = TAKE_PTR(g);
244 
245         return 0;
246 }
247 
dropin_group_record_by_name(const char * name,const char * path,UserDBFlags flags,GroupRecord ** ret)248 int dropin_group_record_by_name(const char *name, const char *path, UserDBFlags flags, GroupRecord **ret) {
249         _cleanup_free_ char *found_path = NULL;
250         _cleanup_fclose_ FILE *f = NULL;
251         int r;
252 
253         assert(name);
254 
255         if (path) {
256                 f = fopen(path, "re");
257                 if (!f)
258                         return errno == ENOENT ? -ESRCH : -errno;
259         } else {
260                 const char *j;
261 
262                 j = strjoina(name, ".group");
263                 if (!filename_is_valid(j)) /* Doesn't qualify as valid filename? Then it's definitely not provided as a drop-in */
264                         return -ESRCH;
265 
266                 r = search_and_fopen_nulstr(j, "re", NULL, USERDB_DROPIN_DIR_NULSTR("userdb"), &f, &found_path);
267                 if (r == -ENOENT)
268                         return -ESRCH;
269                 if (r < 0)
270                         return r;
271 
272                 path = found_path;
273         }
274 
275         return load_group(f, path, name, GID_INVALID, flags, ret);
276 }
277 
dropin_group_record_by_gid(gid_t gid,const char * path,UserDBFlags flags,GroupRecord ** ret)278 int dropin_group_record_by_gid(gid_t gid, const char *path, UserDBFlags flags, GroupRecord **ret) {
279         _cleanup_free_ char *found_path = NULL;
280         _cleanup_fclose_ FILE *f = NULL;
281         int r;
282 
283         assert(gid_is_valid(gid));
284 
285         if (path) {
286                 f = fopen(path, "re");
287                 if (!f)
288                         return errno == ENOENT ? -ESRCH : -errno;
289         } else {
290                 char buf[DECIMAL_STR_MAX(gid_t) + STRLEN(".group") + 1];
291 
292                 xsprintf(buf, GID_FMT ".group", gid);
293 
294                 r = search_and_fopen_nulstr(buf, "re", NULL, USERDB_DROPIN_DIR_NULSTR("userdb"), &f, &found_path);
295                 if (r == -ENOENT)
296                         return -ESRCH;
297                 if (r < 0)
298                         return r;
299 
300                 path = found_path;
301         }
302 
303         return load_group(f, path, NULL, gid, flags, ret);
304 }
305