Adding group-auth patch; updating patches.
[rsync/rsync-patches.git] / group-auth.diff
diff --git a/group-auth.diff b/group-auth.diff
new file mode 100644 (file)
index 0000000..f1bac4f
--- /dev/null
@@ -0,0 +1,416 @@
+This patch adds the ability to do group authentications to the "auth users"
+line via the @group idiom and to specify a group password via an @group entry
+in the secrets file (though the latter is not required if you wish to have
+per-user passwords).  It also allows you to override a module's read-only or
+read-write setting on a per auth-entry basis by adding a :ro or :rw suffix to a
+username or a @groupname.
+
+To use this patch, run these commands for a successful build:
+
+    patch -p1 <patches/group-auth.diff
+    ./configure                         (optional if already run)
+    make
+
+based-on: 3b8f8192227b14e708bf535072485e50f4362270
+diff --git a/authenticate.c b/authenticate.c
+--- a/authenticate.c
++++ b/authenticate.c
+@@ -19,7 +19,9 @@
+  */
+ #include "rsync.h"
++#include "itypes.h"
++extern int read_only;
+ extern char *password_file;
+ /***************************************************************************
+@@ -76,25 +78,40 @@ static void gen_challenge(const char *addr, char *challenge)
+       base64_encode(digest, len, challenge, 0);
+ }
++/* Generate an MD4 hash created from the combination of the password
++ * and the challenge string and return it base64-encoded. */
++static void generate_hash(const char *in, const char *challenge, char *out)
++{
++      char buf[MAX_DIGEST_LEN];
++      int len;
++
++      sum_init(0);
++      sum_update(in, strlen(in));
++      sum_update(challenge, strlen(challenge));
++      len = sum_end(buf);
++
++      base64_encode(buf, len, out, 0);
++}
+ /* Return the secret for a user from the secret file, null terminated.
+  * Maximum length is len (not counting the null). */
+-static int get_secret(int module, const char *user, char *secret, int len)
++static const char *check_secret(int module, const char *user, const char *group,
++                              const char *challenge, const char *pass)
+ {
++      char line[1024];
++      char pass2[MAX_DIGEST_LEN*2];
+       const char *fname = lp_secrets_file(module);
+       STRUCT_STAT st;
+       int fd, ok = 1;
+-      const char *p;
+-      char ch, *s;
++      int user_len = strlen(user);
++      int group_len = group ? strlen(group) : 0;
++      char *err;
+-      if (!fname || !*fname)
+-              return 0;
++      if (!fname || !*fname || (fd = open(fname, O_RDONLY)) < 0)
++              return "no secrets file";
+-      if ((fd = open(fname, O_RDONLY)) < 0)
+-              return 0;
+-
+-      if (do_stat(fname, &st) == -1) {
+-              rsyserr(FLOG, errno, "stat(%s)", fname);
++      if (do_fstat(fd, &st) == -1) {
++              rsyserr(FLOG, errno, "fstat(%s)", fname);
+               ok = 0;
+       } else if (lp_strict_modes(module)) {
+               if ((st.st_mode & 06) != 0) {
+@@ -106,50 +123,47 @@ static int get_secret(int module, const char *user, char *secret, int len)
+               }
+       }
+       if (!ok) {
+-              rprintf(FLOG, "continuing without secrets file\n");
+               close(fd);
+-              return 0;
++              return "ignoring secrets file";
+       }
+       if (*user == '#') {
+               /* Reject attempt to match a comment. */
+               close(fd);
+-              return 0;
++              return "invalid username";
+       }
+-      /* Try to find a line that starts with the user name and a ':'. */
+-      p = user;
+-      while (1) {
+-              if (read(fd, &ch, 1) != 1) {
+-                      close(fd);
+-                      return 0;
++      /* Try to find a line that starts with the user (or @group) name and a ':'. */
++      err = "secret not found";
++      while ((user || group) && read_line_old(fd, line, sizeof line)) {
++              const char **ptr, *s;
++              int len;
++              if (*line == '@') {
++                      ptr = &group;
++                      len = group_len;
++                      s = line+1;
++              } else {
++                      ptr = &user;
++                      len = user_len;
++                      s = line;
+               }
+-              if (ch == '\n')
+-                      p = user;
+-              else if (p) {
+-                      if (*p == ch)
+-                              p++;
+-                      else if (!*p && ch == ':')
+-                              break;
+-                      else
+-                              p = NULL;
++              if (!*ptr || strncmp(s, *ptr, len) != 0 || s[len] != ':')
++                      continue;
++              generate_hash(s+len+1, challenge, pass2);
++              if (strcmp(pass, pass2) == 0) {
++                      err = NULL;
++                      break;
+               }
++              err = "password mismatch";
++              *ptr = NULL; /* Don't look for name again. */
+       }
+-      /* Slurp the secret into the "secret" buffer. */
+-      s = secret;
+-      while (len > 0) {
+-              if (read(fd, s, 1) != 1 || *s == '\n')
+-                      break;
+-              if (*s == '\r')
+-                      continue;
+-              s++;
+-              len--;
+-      }
+-      *s = '\0';
+       close(fd);
+-      return 1;
++      memset(line, 0, sizeof line);
++      memset(pass2, 0, sizeof pass2);
++
++      return err;
+ }
+ static const char *getpassf(const char *filename)
+@@ -199,21 +213,6 @@ static const char *getpassf(const char *filename)
+       return NULL;
+ }
+-/* Generate an MD4 hash created from the combination of the password
+- * and the challenge string and return it base64-encoded. */
+-static void generate_hash(const char *in, const char *challenge, char *out)
+-{
+-      char buf[MAX_DIGEST_LEN];
+-      int len;
+-
+-      sum_init(0);
+-      sum_update(in, strlen(in));
+-      sum_update(challenge, strlen(challenge));
+-      len = sum_end(buf);
+-
+-      base64_encode(buf, len, out, 0);
+-}
+-
+ /* Possibly negotiate authentication with the client.  Use "leader" to
+  * start off the auth if necessary.
+  *
+@@ -226,9 +225,12 @@ char *auth_server(int f_in, int f_out, int module, const char *host,
+       char *users = lp_auth_users(module);
+       char challenge[MAX_DIGEST_LEN*2];
+       char line[BIGPATHBUFLEN];
+-      char secret[512];
+-      char pass2[MAX_DIGEST_LEN*2];
++      char **auth_uid_groups = NULL;
++      int auth_uid_groups_cnt = -1;
++      const char *err = NULL;
++      int group_match = -1;
+       char *tok, *pass;
++      char opt_ch = '\0';
+       /* if no auth list then allow anyone in! */
+       if (!users || !*users)
+@@ -251,37 +253,92 @@ char *auth_server(int f_in, int f_out, int module, const char *host,
+               out_of_memory("auth_server");
+       for (tok = strtok(users, " ,\t"); tok; tok = strtok(NULL, " ,\t")) {
+-              if (wildmatch(tok, line))
+-                      break;
++              char *opts;
++              /* See if the user appended :deny, :ro, or :rw. */
++              if ((opts = strchr(tok, ':')) != NULL) {
++                      *opts++ = '\0';
++                      opt_ch = isUpper(opts) ? toLower(opts) : *opts;
++                      if (opt_ch == 'r') { /* handle ro and rw */
++                              opt_ch = isUpper(opts+1) ? toLower(opts+1) : opts[1];
++                              if (opt_ch == 'o')
++                                      opt_ch = 'r';
++                              else if (opt_ch != 'w')
++                                      opt_ch = '\0';
++                      } else if (opt_ch != 'd') /* if it's not deny, ignore it */
++                              opt_ch = '\0';
++              } else
++                      opt_ch = '\0';
++              if (*tok != '@') {
++                      /* Match the username */
++                      if (wildmatch(tok, line))
++                              break;
++              } else {
++#ifdef HAVE_GETGROUPLIST
++                      int j;
++                      /* See if authorizing user is a real user, and if so, see
++                       * if it is in a group that matches tok+1 wildmat. */
++                      if (auth_uid_groups_cnt < 0) {
++                              gid_t gid_list[64];
++                              uid_t auth_uid;
++                              auth_uid_groups_cnt = sizeof gid_list / sizeof (gid_t);
++                              if (!user_to_uid(line, &auth_uid, False)
++                               || getallgroups(auth_uid, gid_list, &auth_uid_groups_cnt) != NULL)
++                                      auth_uid_groups_cnt = 0;
++                              else {
++                                      if ((auth_uid_groups = new_array(char *, auth_uid_groups_cnt)) == NULL)
++                                              out_of_memory("auth_server");
++                                      for (j = 0; j < auth_uid_groups_cnt; j++)
++                                              auth_uid_groups[j] = gid_to_group(gid_list[j]);
++                              }
++                      }
++                      for (j = 0; j < auth_uid_groups_cnt; j++) {
++                              if (auth_uid_groups[j] && wildmatch(tok+1, auth_uid_groups[j])) {
++                                      group_match = j;
++                                      break;
++                              }
++                      }
++                      if (group_match >= 0)
++                              break;
++#else
++                      rprintf(FLOG, "your computer doesn't support getgrouplist(), so no @group authorization is possible.\n");
++#endif
++              }
+       }
++
+       free(users);
+-      if (!tok) {
+-              rprintf(FLOG, "auth failed on module %s from %s (%s): "
+-                      "unauthorized user\n",
+-                      lp_name(module), host, addr);
+-              return NULL;
++      if (!tok)
++              err = "no matching rule";
++      else if (opt_ch == 'd')
++              err = "denied by rule";
++      else {
++              char *group = group_match >= 0 ? auth_uid_groups[group_match] : NULL;
++              err = check_secret(module, line, group, challenge, pass);
+       }
+-      memset(secret, 0, sizeof secret);
+-      if (!get_secret(module, line, secret, sizeof secret - 1)) {
+-              memset(secret, 0, sizeof secret);
+-              rprintf(FLOG, "auth failed on module %s from %s (%s): "
+-                      "missing secret for user \"%s\"\n",
+-                      lp_name(module), host, addr, line);
+-              return NULL;
+-      }
++      memset(challenge, 0, sizeof challenge);
++      memset(pass, 0, strlen(pass));
+-      generate_hash(secret, challenge, pass2);
+-      memset(secret, 0, sizeof secret);
++      if (auth_uid_groups) {
++              int j;
++              for (j = 0; j < auth_uid_groups_cnt; j++) {
++                      if (auth_uid_groups[j])
++                              free(auth_uid_groups[j]);
++              }
++              free(auth_uid_groups);
++      }
+-      if (strcmp(pass, pass2) != 0) {
+-              rprintf(FLOG, "auth failed on module %s from %s (%s): "
+-                      "password mismatch\n",
+-                      lp_name(module), host, addr);
++      if (err) {
++              rprintf(FLOG, "auth failed on module %s from %s (%s) for %s: %s\n",
++                      lp_name(module), host, addr, line, err);
+               return NULL;
+       }
++      if (opt_ch == 'r')
++              read_only = 1;
++      else if (opt_ch == 'w')
++              read_only = 0;
++
+       return strdup(line);
+ }
+diff --git a/clientserver.c b/clientserver.c
+--- a/clientserver.c
++++ b/clientserver.c
+@@ -546,6 +546,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
+               return -1;
+       }
++      read_only = lp_read_only(i); /* may also be overridden by auth_server() */
+       auth_user = auth_server(f_in, f_out, i, host, addr, "@RSYNCD: AUTHREQD ");
+       if (!auth_user) {
+@@ -556,9 +557,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
+       module_id = i;
+-      if (lp_read_only(i))
+-              read_only = 1;
+-
+       if (lp_transfer_logging(i) && !logfile_format)
+               logfile_format = lp_log_format(i);
+       if (log_format_has(logfile_format, 'i'))
+diff --git a/rsyncd.conf.yo b/rsyncd.conf.yo
+--- a/rsyncd.conf.yo
++++ b/rsyncd.conf.yo
+@@ -318,6 +318,8 @@ attempted uploads will fail. If "read only" is false then uploads will
+ be possible if file permissions on the daemon side allow them. The default
+ is for all modules to be read only.
++Note that "auth users" can override this setting on a per-user basis.
++
+ dit(bf(write only)) This parameter determines whether clients
+ will be able to download files or not. If "write only" is true then any
+ attempted downloads will fail. If "write only" is false then downloads
+@@ -430,10 +432,12 @@ be on to the clients.
+ See the description of the bf(--chmod) rsync option and the bf(chmod)(1)
+ manpage for information on the format of this string.
+-dit(bf(auth users)) This parameter specifies a comma and
+-space-separated list of usernames that will be allowed to connect to
++dit(bf(auth users)) This parameter specifies a comma and/or space-separated
++list of authorization rules.  In its simplest form, you list the usernames
++that will be allowed to connect to
+ this module. The usernames do not need to exist on the local
+-system. The usernames may also contain shell wildcard characters. If
++system. The rules may contain shell wildcard characters that will be matched
++against the username provided by the client for authentication. If
+ "auth users" is set then the client will be challenged to supply a
+ username and password to connect to the module. A challenge response
+ authentication protocol is used for this exchange. The plain text
+@@ -441,20 +445,50 @@ usernames and passwords are stored in the file specified by the
+ "secrets file" parameter. The default is for all users to be able to
+ connect without a password (this is called "anonymous rsync").
++In addition to username matching, you can specify groupname matching via a '@'
++prefix.  When using groupname matching, the authenticating username must be a
++real user on the system, or it will be assumed to be a member of no groups.
++For example, specifying "@rsync" will match the authenticating user if the
++named user is a member of the rsync group.
++
++Finally, options may be specified after a colon (:).  The options allow you to
++"deny" a user or a group, set the access to "ro" (read-only), or set the access
++to "rw" (read/write).  Setting an auth-rule-specific ro/rw setting overrides
++the module's default "read only" setting.
++
++Be sure to put the rules in the order you want them to be matched, because the
++checking stops at the first match.  For example:
++
++verb(  auth users = joe:deny @guest:deny admin:rw @rsync:ro susan )
++
++In the above rule, user joe will be denied access no matter what.  Any user
++that is in the group "guest" is also denied access.  The user "admin" gets
++access in read/write mode, even if the admin user is in group rsync (because
++the admin user-matching rule is before the rsync group-matching rule).
++Finally, user susan gets the default ro/rw setting of the module, but only
++if susan's user didn't match an earlier group-matching rule.
++
++See the description of the secrets file for how you can have per-user passwords
++as well as per-group passwords (either or both).
++
+ See also the section entitled "USING RSYNC-DAEMON FEATURES VIA A REMOTE
+ SHELL CONNECTION" in bf(rsync)(1) for information on how handle an
+ rsyncd.conf-level username that differs from the remote-shell-level
+ username when using a remote shell to connect to an rsync daemon.
+-dit(bf(secrets file)) This parameter specifies the name of
+-a file that contains the username:password pairs used for
+-authenticating this module. This file is only consulted if the "auth
+-users" parameter is specified. The file is line based and contains
+-username:password pairs separated by a single colon. Any line starting
+-with a hash (#) is considered a comment and is skipped. The passwords
+-can contain any characters but be warned that many operating systems
+-limit the length of passwords that can be typed at the client end, so
+-you may find that passwords longer than 8 characters don't work.
++dit(bf(secrets file)) This parameter specifies the name of a file that contains
++the username:password and/or @group:password pairs used for authenticating this
++module. This file is only consulted if the "auth users" parameter is specified.
++The file is line-based and contains one name:password pair per line.  Any line
++has a hash (#) as the very first character on the line is considered a comment
++and is skipped.  The passwords can contain any characters but be warned that
++many operating systems limit the length of passwords that can be typed at the
++client end, so you may find that passwords longer than 8 characters don't work.
++
++The use of group-specific lines are only relevant when the module was
++authorized using a matching "@group" rule.  When that happens, the user can be
++authorized via either their "username:password" line or the "@group:password"
++line for the group that triggered the authentication.
+ There is no default for the "secrets file" parameter, you must choose a name
+ (such as tt(/etc/rsyncd.secrets)).  The file must normally not be readable