Commit | Line | Data |
---|---|---|
72e5645e WD |
1 | This patch adds the ability to do group authentications to the "auth users" |
2 | line via the @group idiom and to specify a group password via an @group entry | |
3 | in the secrets file (though the latter is not required if you wish to have | |
4 | per-user passwords). It also allows you to override a module's read-only or | |
5 | read-write setting on a per auth-entry basis by adding a :ro or :rw suffix to a | |
6 | username or a @groupname. | |
7 | ||
8 | To use this patch, run these commands for a successful build: | |
9 | ||
10 | patch -p1 <patches/group-auth.diff | |
11 | ./configure (optional if already run) | |
12 | make | |
13 | ||
c1ff70aa | 14 | based-on: a01e3b490eb36ccf9e704840e1b6683dab867550 |
72e5645e WD |
15 | diff --git a/authenticate.c b/authenticate.c |
16 | --- a/authenticate.c | |
17 | +++ b/authenticate.c | |
18 | @@ -19,7 +19,9 @@ | |
19 | */ | |
20 | ||
21 | #include "rsync.h" | |
22 | +#include "itypes.h" | |
23 | ||
24 | +extern int read_only; | |
25 | extern char *password_file; | |
26 | ||
27 | /*************************************************************************** | |
28 | @@ -76,25 +78,40 @@ static void gen_challenge(const char *addr, char *challenge) | |
29 | base64_encode(digest, len, challenge, 0); | |
30 | } | |
31 | ||
32 | +/* Generate an MD4 hash created from the combination of the password | |
33 | + * and the challenge string and return it base64-encoded. */ | |
34 | +static void generate_hash(const char *in, const char *challenge, char *out) | |
35 | +{ | |
36 | + char buf[MAX_DIGEST_LEN]; | |
37 | + int len; | |
38 | + | |
39 | + sum_init(0); | |
40 | + sum_update(in, strlen(in)); | |
41 | + sum_update(challenge, strlen(challenge)); | |
42 | + len = sum_end(buf); | |
43 | + | |
44 | + base64_encode(buf, len, out, 0); | |
45 | +} | |
46 | ||
47 | /* Return the secret for a user from the secret file, null terminated. | |
48 | * Maximum length is len (not counting the null). */ | |
49 | -static int get_secret(int module, const char *user, char *secret, int len) | |
50 | +static const char *check_secret(int module, const char *user, const char *group, | |
51 | + const char *challenge, const char *pass) | |
52 | { | |
53 | + char line[1024]; | |
54 | + char pass2[MAX_DIGEST_LEN*2]; | |
55 | const char *fname = lp_secrets_file(module); | |
56 | STRUCT_STAT st; | |
57 | int fd, ok = 1; | |
58 | - const char *p; | |
59 | - char ch, *s; | |
60 | + int user_len = strlen(user); | |
61 | + int group_len = group ? strlen(group) : 0; | |
62 | + char *err; | |
63 | ||
64 | - if (!fname || !*fname) | |
65 | - return 0; | |
66 | + if (!fname || !*fname || (fd = open(fname, O_RDONLY)) < 0) | |
67 | + return "no secrets file"; | |
68 | ||
69 | - if ((fd = open(fname, O_RDONLY)) < 0) | |
70 | - return 0; | |
71 | - | |
72 | - if (do_stat(fname, &st) == -1) { | |
73 | - rsyserr(FLOG, errno, "stat(%s)", fname); | |
74 | + if (do_fstat(fd, &st) == -1) { | |
75 | + rsyserr(FLOG, errno, "fstat(%s)", fname); | |
76 | ok = 0; | |
77 | } else if (lp_strict_modes(module)) { | |
78 | if ((st.st_mode & 06) != 0) { | |
79 | @@ -106,50 +123,47 @@ static int get_secret(int module, const char *user, char *secret, int len) | |
80 | } | |
81 | } | |
82 | if (!ok) { | |
83 | - rprintf(FLOG, "continuing without secrets file\n"); | |
84 | close(fd); | |
85 | - return 0; | |
86 | + return "ignoring secrets file"; | |
87 | } | |
88 | ||
89 | if (*user == '#') { | |
90 | /* Reject attempt to match a comment. */ | |
91 | close(fd); | |
92 | - return 0; | |
93 | + return "invalid username"; | |
94 | } | |
95 | ||
96 | - /* Try to find a line that starts with the user name and a ':'. */ | |
97 | - p = user; | |
98 | - while (1) { | |
99 | - if (read(fd, &ch, 1) != 1) { | |
100 | - close(fd); | |
101 | - return 0; | |
102 | + /* Try to find a line that starts with the user (or @group) name and a ':'. */ | |
103 | + err = "secret not found"; | |
104 | + while ((user || group) && read_line_old(fd, line, sizeof line)) { | |
105 | + const char **ptr, *s; | |
106 | + int len; | |
107 | + if (*line == '@') { | |
108 | + ptr = &group; | |
109 | + len = group_len; | |
110 | + s = line+1; | |
111 | + } else { | |
112 | + ptr = &user; | |
113 | + len = user_len; | |
114 | + s = line; | |
115 | } | |
116 | - if (ch == '\n') | |
117 | - p = user; | |
118 | - else if (p) { | |
119 | - if (*p == ch) | |
120 | - p++; | |
121 | - else if (!*p && ch == ':') | |
122 | - break; | |
123 | - else | |
124 | - p = NULL; | |
125 | + if (!*ptr || strncmp(s, *ptr, len) != 0 || s[len] != ':') | |
126 | + continue; | |
127 | + generate_hash(s+len+1, challenge, pass2); | |
128 | + if (strcmp(pass, pass2) == 0) { | |
129 | + err = NULL; | |
130 | + break; | |
131 | } | |
132 | + err = "password mismatch"; | |
133 | + *ptr = NULL; /* Don't look for name again. */ | |
134 | } | |
135 | ||
136 | - /* Slurp the secret into the "secret" buffer. */ | |
137 | - s = secret; | |
138 | - while (len > 0) { | |
139 | - if (read(fd, s, 1) != 1 || *s == '\n') | |
140 | - break; | |
141 | - if (*s == '\r') | |
142 | - continue; | |
143 | - s++; | |
144 | - len--; | |
145 | - } | |
146 | - *s = '\0'; | |
147 | close(fd); | |
148 | ||
149 | - return 1; | |
150 | + memset(line, 0, sizeof line); | |
151 | + memset(pass2, 0, sizeof pass2); | |
152 | + | |
153 | + return err; | |
154 | } | |
155 | ||
156 | static const char *getpassf(const char *filename) | |
157 | @@ -199,21 +213,6 @@ static const char *getpassf(const char *filename) | |
158 | return NULL; | |
159 | } | |
160 | ||
161 | -/* Generate an MD4 hash created from the combination of the password | |
162 | - * and the challenge string and return it base64-encoded. */ | |
163 | -static void generate_hash(const char *in, const char *challenge, char *out) | |
164 | -{ | |
165 | - char buf[MAX_DIGEST_LEN]; | |
166 | - int len; | |
167 | - | |
168 | - sum_init(0); | |
169 | - sum_update(in, strlen(in)); | |
170 | - sum_update(challenge, strlen(challenge)); | |
171 | - len = sum_end(buf); | |
172 | - | |
173 | - base64_encode(buf, len, out, 0); | |
174 | -} | |
175 | - | |
176 | /* Possibly negotiate authentication with the client. Use "leader" to | |
177 | * start off the auth if necessary. | |
178 | * | |
179 | @@ -226,9 +225,12 @@ char *auth_server(int f_in, int f_out, int module, const char *host, | |
180 | char *users = lp_auth_users(module); | |
181 | char challenge[MAX_DIGEST_LEN*2]; | |
182 | char line[BIGPATHBUFLEN]; | |
183 | - char secret[512]; | |
184 | - char pass2[MAX_DIGEST_LEN*2]; | |
185 | + char **auth_uid_groups = NULL; | |
186 | + int auth_uid_groups_cnt = -1; | |
187 | + const char *err = NULL; | |
188 | + int group_match = -1; | |
189 | char *tok, *pass; | |
190 | + char opt_ch = '\0'; | |
191 | ||
192 | /* if no auth list then allow anyone in! */ | |
193 | if (!users || !*users) | |
194 | @@ -251,37 +253,92 @@ char *auth_server(int f_in, int f_out, int module, const char *host, | |
195 | out_of_memory("auth_server"); | |
196 | ||
197 | for (tok = strtok(users, " ,\t"); tok; tok = strtok(NULL, " ,\t")) { | |
198 | - if (wildmatch(tok, line)) | |
199 | - break; | |
200 | + char *opts; | |
201 | + /* See if the user appended :deny, :ro, or :rw. */ | |
202 | + if ((opts = strchr(tok, ':')) != NULL) { | |
203 | + *opts++ = '\0'; | |
204 | + opt_ch = isUpper(opts) ? toLower(opts) : *opts; | |
205 | + if (opt_ch == 'r') { /* handle ro and rw */ | |
206 | + opt_ch = isUpper(opts+1) ? toLower(opts+1) : opts[1]; | |
207 | + if (opt_ch == 'o') | |
208 | + opt_ch = 'r'; | |
209 | + else if (opt_ch != 'w') | |
210 | + opt_ch = '\0'; | |
211 | + } else if (opt_ch != 'd') /* if it's not deny, ignore it */ | |
212 | + opt_ch = '\0'; | |
213 | + } else | |
214 | + opt_ch = '\0'; | |
215 | + if (*tok != '@') { | |
216 | + /* Match the username */ | |
217 | + if (wildmatch(tok, line)) | |
218 | + break; | |
219 | + } else { | |
220 | +#ifdef HAVE_GETGROUPLIST | |
221 | + int j; | |
222 | + /* See if authorizing user is a real user, and if so, see | |
223 | + * if it is in a group that matches tok+1 wildmat. */ | |
224 | + if (auth_uid_groups_cnt < 0) { | |
225 | + gid_t gid_list[64]; | |
226 | + uid_t auth_uid; | |
227 | + auth_uid_groups_cnt = sizeof gid_list / sizeof (gid_t); | |
228 | + if (!user_to_uid(line, &auth_uid, False) | |
229 | + || getallgroups(auth_uid, gid_list, &auth_uid_groups_cnt) != NULL) | |
230 | + auth_uid_groups_cnt = 0; | |
231 | + else { | |
232 | + if ((auth_uid_groups = new_array(char *, auth_uid_groups_cnt)) == NULL) | |
233 | + out_of_memory("auth_server"); | |
234 | + for (j = 0; j < auth_uid_groups_cnt; j++) | |
235 | + auth_uid_groups[j] = gid_to_group(gid_list[j]); | |
236 | + } | |
237 | + } | |
238 | + for (j = 0; j < auth_uid_groups_cnt; j++) { | |
239 | + if (auth_uid_groups[j] && wildmatch(tok+1, auth_uid_groups[j])) { | |
240 | + group_match = j; | |
241 | + break; | |
242 | + } | |
243 | + } | |
244 | + if (group_match >= 0) | |
245 | + break; | |
246 | +#else | |
5214a41b | 247 | + rprintf(FLOG, "your computer doesn't support getgrouplist(), so no @group authorization is possible.\n"); |
72e5645e WD |
248 | +#endif |
249 | + } | |
250 | } | |
251 | + | |
252 | free(users); | |
253 | ||
254 | - if (!tok) { | |
255 | - rprintf(FLOG, "auth failed on module %s from %s (%s): " | |
256 | - "unauthorized user\n", | |
257 | - lp_name(module), host, addr); | |
258 | - return NULL; | |
259 | + if (!tok) | |
260 | + err = "no matching rule"; | |
261 | + else if (opt_ch == 'd') | |
262 | + err = "denied by rule"; | |
263 | + else { | |
264 | + char *group = group_match >= 0 ? auth_uid_groups[group_match] : NULL; | |
265 | + err = check_secret(module, line, group, challenge, pass); | |
266 | } | |
267 | ||
268 | - memset(secret, 0, sizeof secret); | |
269 | - if (!get_secret(module, line, secret, sizeof secret - 1)) { | |
270 | - memset(secret, 0, sizeof secret); | |
271 | - rprintf(FLOG, "auth failed on module %s from %s (%s): " | |
272 | - "missing secret for user \"%s\"\n", | |
273 | - lp_name(module), host, addr, line); | |
274 | - return NULL; | |
275 | - } | |
276 | + memset(challenge, 0, sizeof challenge); | |
277 | + memset(pass, 0, strlen(pass)); | |
278 | ||
279 | - generate_hash(secret, challenge, pass2); | |
280 | - memset(secret, 0, sizeof secret); | |
281 | + if (auth_uid_groups) { | |
282 | + int j; | |
283 | + for (j = 0; j < auth_uid_groups_cnt; j++) { | |
284 | + if (auth_uid_groups[j]) | |
285 | + free(auth_uid_groups[j]); | |
286 | + } | |
287 | + free(auth_uid_groups); | |
288 | + } | |
289 | ||
290 | - if (strcmp(pass, pass2) != 0) { | |
291 | - rprintf(FLOG, "auth failed on module %s from %s (%s): " | |
292 | - "password mismatch\n", | |
293 | - lp_name(module), host, addr); | |
294 | + if (err) { | |
295 | + rprintf(FLOG, "auth failed on module %s from %s (%s) for %s: %s\n", | |
296 | + lp_name(module), host, addr, line, err); | |
297 | return NULL; | |
298 | } | |
299 | ||
300 | + if (opt_ch == 'r') | |
301 | + read_only = 1; | |
302 | + else if (opt_ch == 'w') | |
303 | + read_only = 0; | |
304 | + | |
305 | return strdup(line); | |
306 | } | |
307 | ||
308 | diff --git a/clientserver.c b/clientserver.c | |
309 | --- a/clientserver.c | |
310 | +++ b/clientserver.c | |
5214a41b | 311 | @@ -545,6 +545,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char |
72e5645e WD |
312 | return -1; |
313 | } | |
314 | ||
315 | + read_only = lp_read_only(i); /* may also be overridden by auth_server() */ | |
316 | auth_user = auth_server(f_in, f_out, i, host, addr, "@RSYNCD: AUTHREQD "); | |
317 | ||
318 | if (!auth_user) { | |
5214a41b | 319 | @@ -555,9 +556,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char |
72e5645e WD |
320 | |
321 | module_id = i; | |
322 | ||
323 | - if (lp_read_only(i)) | |
324 | - read_only = 1; | |
325 | - | |
326 | if (lp_transfer_logging(i) && !logfile_format) | |
327 | logfile_format = lp_log_format(i); | |
328 | if (log_format_has(logfile_format, 'i')) | |
329 | diff --git a/rsyncd.conf.yo b/rsyncd.conf.yo | |
330 | --- a/rsyncd.conf.yo | |
331 | +++ b/rsyncd.conf.yo | |
332 | @@ -318,6 +318,8 @@ attempted uploads will fail. If "read only" is false then uploads will | |
333 | be possible if file permissions on the daemon side allow them. The default | |
334 | is for all modules to be read only. | |
335 | ||
336 | +Note that "auth users" can override this setting on a per-user basis. | |
337 | + | |
338 | dit(bf(write only)) This parameter determines whether clients | |
339 | will be able to download files or not. If "write only" is true then any | |
340 | attempted downloads will fail. If "write only" is false then downloads | |
341 | @@ -430,10 +432,12 @@ be on to the clients. | |
342 | See the description of the bf(--chmod) rsync option and the bf(chmod)(1) | |
343 | manpage for information on the format of this string. | |
344 | ||
345 | -dit(bf(auth users)) This parameter specifies a comma and | |
346 | -space-separated list of usernames that will be allowed to connect to | |
347 | +dit(bf(auth users)) This parameter specifies a comma and/or space-separated | |
348 | +list of authorization rules. In its simplest form, you list the usernames | |
349 | +that will be allowed to connect to | |
350 | this module. The usernames do not need to exist on the local | |
351 | -system. The usernames may also contain shell wildcard characters. If | |
352 | +system. The rules may contain shell wildcard characters that will be matched | |
353 | +against the username provided by the client for authentication. If | |
354 | "auth users" is set then the client will be challenged to supply a | |
355 | username and password to connect to the module. A challenge response | |
356 | authentication protocol is used for this exchange. The plain text | |
357 | @@ -441,20 +445,50 @@ usernames and passwords are stored in the file specified by the | |
358 | "secrets file" parameter. The default is for all users to be able to | |
359 | connect without a password (this is called "anonymous rsync"). | |
360 | ||
361 | +In addition to username matching, you can specify groupname matching via a '@' | |
362 | +prefix. When using groupname matching, the authenticating username must be a | |
363 | +real user on the system, or it will be assumed to be a member of no groups. | |
364 | +For example, specifying "@rsync" will match the authenticating user if the | |
365 | +named user is a member of the rsync group. | |
366 | + | |
367 | +Finally, options may be specified after a colon (:). The options allow you to | |
368 | +"deny" a user or a group, set the access to "ro" (read-only), or set the access | |
369 | +to "rw" (read/write). Setting an auth-rule-specific ro/rw setting overrides | |
370 | +the module's default "read only" setting. | |
371 | + | |
372 | +Be sure to put the rules in the order you want them to be matched, because the | |
373 | +checking stops at the first match. For example: | |
374 | + | |
375 | +verb( auth users = joe:deny @guest:deny admin:rw @rsync:ro susan ) | |
376 | + | |
5214a41b | 377 | +In the above rule, user joe will be denied access no matter what. Any user |
72e5645e WD |
378 | +that is in the group "guest" is also denied access. The user "admin" gets |
379 | +access in read/write mode, even if the admin user is in group rsync (because | |
380 | +the admin user-matching rule is before the rsync group-matching rule). | |
381 | +Finally, user susan gets the default ro/rw setting of the module, but only | |
382 | +if susan's user didn't match an earlier group-matching rule. | |
383 | + | |
384 | +See the description of the secrets file for how you can have per-user passwords | |
385 | +as well as per-group passwords (either or both). | |
386 | + | |
387 | See also the section entitled "USING RSYNC-DAEMON FEATURES VIA A REMOTE | |
388 | SHELL CONNECTION" in bf(rsync)(1) for information on how handle an | |
389 | rsyncd.conf-level username that differs from the remote-shell-level | |
390 | username when using a remote shell to connect to an rsync daemon. | |
391 | ||
392 | -dit(bf(secrets file)) This parameter specifies the name of | |
393 | -a file that contains the username:password pairs used for | |
394 | -authenticating this module. This file is only consulted if the "auth | |
395 | -users" parameter is specified. The file is line based and contains | |
396 | -username:password pairs separated by a single colon. Any line starting | |
397 | -with a hash (#) is considered a comment and is skipped. The passwords | |
398 | -can contain any characters but be warned that many operating systems | |
399 | -limit the length of passwords that can be typed at the client end, so | |
400 | -you may find that passwords longer than 8 characters don't work. | |
401 | +dit(bf(secrets file)) This parameter specifies the name of a file that contains | |
402 | +the username:password and/or @group:password pairs used for authenticating this | |
403 | +module. This file is only consulted if the "auth users" parameter is specified. | |
404 | +The file is line-based and contains one name:password pair per line. Any line | |
405 | +has a hash (#) as the very first character on the line is considered a comment | |
406 | +and is skipped. The passwords can contain any characters but be warned that | |
407 | +many operating systems limit the length of passwords that can be typed at the | |
408 | +client end, so you may find that passwords longer than 8 characters don't work. | |
409 | + | |
410 | +The use of group-specific lines are only relevant when the module was | |
411 | +authorized using a matching "@group" rule. When that happens, the user can be | |
412 | +authorized via either their "username:password" line or the "@group:password" | |
413 | +line for the group that triggered the authentication. | |
414 | ||
415 | There is no default for the "secrets file" parameter, you must choose a name | |
416 | (such as tt(/etc/rsyncd.secrets)). The file must normally not be readable |