Index: subversion/include/svn_config.h =================================================================== --- subversion/include/svn_config.h (revision 1437378) +++ subversion/include/svn_config.h (working copy) @@ -149,6 +149,7 @@ typedef struct svn_config_t svn_config_t; #define SVN_CONFIG_OPTION_USE_SASL "use-sasl" #define SVN_CONFIG_OPTION_MIN_SSF "min-encryption" #define SVN_CONFIG_OPTION_MAX_SSF "max-encryption" +#define SVN_CONFIG_OPTION_GROUPS_DB "groups-db" /* For repository password database */ #define SVN_CONFIG_SECTION_USERS "users" Index: subversion/include/svn_repos.h =================================================================== --- subversion/include/svn_repos.h (revision 1437378) +++ subversion/include/svn_repos.h (working copy) @@ -3171,19 +3171,24 @@ svn_repos_authz_read(svn_authz_t **authz_p, * url, an absolute file url, or a registry path) into @a *authz_p, * allocated in @a pool. * - * If @a path is not a valid authz rule file, then return + * If @a groups_path (a file, repos relative url, an absolute file url, + * or a registry path) is set, use the global groups parsed from it. + * + * If @a path or @a groups_path is not a valid authz rule file, then return * #SVN_ERR_AUTHZ_INVALID_CONFIG. The contents of @a *authz_p is then - * undefined. If @a must_exist is TRUE, a missing authz file is also - * an error. + * undefined. If @a must_exist is TRUE, a missing authz or groups file + * is also an error. * * If @a path is a repos relative URL then @a repos_root must be set to * the root of the repository the authz configuration will be used with. + * The same applies to @a groups_path if it is being used. * * @since New in 1.8 */ svn_error_t * svn_repos_authz_read2(svn_authz_t **authz_p, const char *path, + const char *groups_path, svn_boolean_t must_exist, const char *repos_root, apr_pool_t *pool); @@ -3193,11 +3198,14 @@ svn_repos_authz_read2(svn_authz_t **authz_p, * Read authz configuration data from @a stream into @a *authz_p, * allocated in @a pool. * + * If @a groups_stream is set, use the global groups parsed from it. + * * @since New in 1.8 */ svn_error_t * svn_repos_authz_parse(svn_authz_t **authz_p, svn_stream_t *stream, + svn_stream_t *groups_stream, apr_pool_t *pool); /** Index: subversion/libsvn_repos/authz.c =================================================================== --- subversion/libsvn_repos/authz.c (revision 1437378) +++ subversion/libsvn_repos/authz.c (working copy) @@ -919,20 +919,81 @@ authz_retrieve_config(svn_config_t **cfg_p, const return SVN_NO_ERROR; } + +/* Callback to copy (name, value) group into the "groups" section + of another configuration. */ +static svn_boolean_t +authz_copy_group(const char *name, const char *value, + void *baton, apr_pool_t *pool) +{ + svn_config_t *authz_cfg = baton; + + svn_config_set(authz_cfg, SVN_CONFIG_SECTION_GROUPS, name, value); + + return TRUE; +} + +/* Copy group definitions from GROUPS_CFG to the resulting AUTHZ. + * If AUTHZ already contains any group definition, report an error. + * Use POOL for temporary allocations. */ +static svn_error_t * +authz_copy_groups(svn_authz_t *authz, svn_config_t *groups_cfg, + apr_pool_t *pool) +{ + /* Easy out: we prohibit local groups in the authz file when global + groups are being used. */ + if (svn_config_has_section(authz->cfg, SVN_CONFIG_SECTION_GROUPS)) + { + return svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + "Authz file cannot contain any groups " + "when global groups are being used."); + } + + svn_config_enumerate2(groups_cfg, SVN_CONFIG_SECTION_GROUPS, + authz_copy_group, authz->cfg, pool); + + return SVN_NO_ERROR; +} + svn_error_t * svn_repos__authz_read(svn_authz_t **authz_p, const char *path, - svn_boolean_t must_exist, svn_boolean_t accept_urls, - const char *repos_root, apr_pool_t *pool) + const char *groups_path, svn_boolean_t must_exist, + svn_boolean_t accept_urls, const char *repos_root, + apr_pool_t *pool) { svn_authz_t *authz = apr_palloc(pool, sizeof(*authz)); - /* Load the rule file */ + /* Load the authz file */ if (accept_urls) SVN_ERR(authz_retrieve_config(&authz->cfg, path, must_exist, repos_root, pool)); else SVN_ERR(svn_config_read2(&authz->cfg, path, must_exist, TRUE, pool)); + if (groups_path) + { + svn_config_t *groups_cfg; + svn_error_t *err; + + /* Load the groups file */ + if (accept_urls) + SVN_ERR(authz_retrieve_config(&groups_cfg, groups_path, must_exist, + repos_root, pool)); + else + SVN_ERR(svn_config_read2(&groups_cfg, groups_path, must_exist, + TRUE, pool)); + + /* Copy the groups from groups_cfg into authz. */ + err = authz_copy_groups(authz, groups_cfg, pool); + + /* Add the paths to the error stack since the authz_copy_groups + routine knows nothing about them. */ + if (err != SVN_NO_ERROR) + return svn_error_createf(err->apr_err, err, + "Error reading authz file '%s' with " + "groups file '%s':", path, groups_path); + } + /* Make sure there are no errors in the configuration. */ SVN_ERR(authz_validate(authz, pool)); @@ -946,23 +1007,33 @@ svn_repos__authz_read(svn_authz_t **authz_p, const svn_error_t * svn_repos_authz_read2(svn_authz_t **authz_p, const char *path, - svn_boolean_t must_exist, const char *repos_root, - apr_pool_t *pool) + const char *groups_path, svn_boolean_t must_exist, + const char *repos_root, apr_pool_t *pool) { - return svn_repos__authz_read(authz_p, path, must_exist, TRUE, repos_root, - pool); + return svn_repos__authz_read(authz_p, path, groups_path, must_exist, + TRUE, repos_root, pool); } svn_error_t * svn_repos_authz_parse(svn_authz_t **authz_p, svn_stream_t *stream, - apr_pool_t *pool) + svn_stream_t *groups_stream, apr_pool_t *pool) { svn_authz_t *authz = apr_palloc(pool, sizeof(*authz)); - /* Parse the stream */ + /* Parse the authz stream */ SVN_ERR(svn_config_parse(&authz->cfg, stream, TRUE, pool)); + if (groups_stream) + { + svn_config_t *groups_cfg; + + /* Parse the groups stream */ + SVN_ERR(svn_config_parse(&groups_cfg, groups_stream, TRUE, pool)); + + SVN_ERR(authz_copy_groups(authz, groups_cfg, pool)); + } + /* Make sure there are no errors in the configuration. */ SVN_ERR(authz_validate(authz, pool)); Index: subversion/libsvn_repos/deprecated.c =================================================================== --- subversion/libsvn_repos/deprecated.c (revision 1437378) +++ subversion/libsvn_repos/deprecated.c (working copy) @@ -1013,5 +1013,6 @@ svn_error_t * svn_repos_authz_read(svn_authz_t **authz_p, const char *file, svn_boolean_t must_exist, apr_pool_t *pool) { - return svn_repos__authz_read(authz_p, file, must_exist, FALSE, NULL, pool); + return svn_repos__authz_read(authz_p, file, NULL, must_exist, + FALSE, NULL, pool); } Index: subversion/libsvn_repos/repos.c =================================================================== --- subversion/libsvn_repos/repos.c (revision 1437378) +++ subversion/libsvn_repos/repos.c (working copy) @@ -1025,6 +1025,12 @@ create_conf(svn_repos_t *repos, apr_pool_t *pool) "### no path-based access control is done." NL "### Uncomment the line below to use the default authorization file." NL "# authz-db = " SVN_REPOS__CONF_AUTHZ NL +"### The groups-db option controls the location of the groups file." NL +"### Unless you specify a path starting with a /, the file's location is" NL +"### relative to the directory containing this file. The specified path" NL +"### may be a repository relative URL (^/) or an absolute file:// URL to a" NL +"### text file in a Subversion repository." NL +"# groups-db = " SVN_REPOS__CONF_GROUPS NL "### This option specifies the authentication realm of the repository." NL "### If two repositories have the same authentication realm, they should" NL "### have the same password database, and vice versa. The default realm" NL Index: subversion/libsvn_repos/repos.h =================================================================== --- subversion/libsvn_repos/repos.h (revision 1437378) +++ subversion/libsvn_repos/repos.h (working copy) @@ -95,10 +95,11 @@ extern "C" { #define SVN_REPOS__CONF_SVNSERVE_CONF "svnserve.conf" /* In the svnserve default configuration, these are the suggested - locations for the passwd and authz files (in the repository conf - directory), and we put example templates there. */ + locations for the passwd, authz and groups files (in the repository + conf directory), and we put example templates there. */ #define SVN_REPOS__CONF_PASSWD "passwd" #define SVN_REPOS__CONF_AUTHZ "authz" +#define SVN_REPOS__CONF_GROUPS "groups" /* The Repository object, created by svn_repos_open2() and svn_repos_create(). */ @@ -307,22 +308,24 @@ svn_repos__hooks_post_unlock(svn_repos_t *repos, /*** Authz Functions ***/ /* Read authz configuration data from PATH into *AUTHZ_P, allocated - in POOL. - - PATH may be a file or a registry path and iff ACCEPT_URLS is set - it may also be a repos relative url or an absolute file url. When + in POOL. If GROUPS_PATH is set, use the global groups parsed from it. + + PATH and GROUPS_PATH may be a file or a registry path and iff ACCEPT_URLS + is set it may also be a repos relative url or an absolute file url. When ACCEPT_URLS is FALSE REPOS_ROOT can be NULL. - - If PATH is not a valid authz rule file, then return + + If PATH or GROUPS_PATH is not a valid authz rule file, then return SVN_AUTHZ_INVALID_CONFIG. The contents of *AUTHZ_P is then - undefined. If MUST_EXIST is TRUE, a missing authz file is also - an error. - + undefined. If MUST_EXIST is TRUE, a missing authz or global groups file + is also an error. + If PATH is a repos relative URL then REPOS_ROOT must be set to - the root of the repository the authz configuration will be used with. */ + the root of the repository the authz configuration will be used with. + The same applies to GROUPS_PATH if it is being used. */ svn_error_t * svn_repos__authz_read(svn_authz_t **authz_p, const char *path, + const char *groups_path, svn_boolean_t must_exist, svn_boolean_t accept_urls, const char *repos_root, Index: subversion/mod_authz_svn/INSTALL =================================================================== --- subversion/mod_authz_svn/INSTALL (revision 1437378) +++ subversion/mod_authz_svn/INSTALL (working copy) @@ -186,7 +186,25 @@ II. Configuration The "Require" statement in the previous example is not strictly needed, but has been included for clarity. + H. Example 8: Separate authz and groups files. + This configuration allows storing the groups separately from the + main authz file with the authorization rules. + + + DAV svn + SVNParentPath /path/to/reposparent + + AuthType Basic + AuthName "Subversion repository" + AuthUserFile /path/to/htpasswd/file + + AuthzSVNAccessFile /path/to/access/file + AuthzSVNGroupsFile /path/to/groups/file + + Require valid-user + + 2. Specifying permissions The file format of the access file looks like this: Index: subversion/mod_authz_svn/mod_authz_svn.c =================================================================== --- subversion/mod_authz_svn/mod_authz_svn.c (revision 1437378) +++ subversion/mod_authz_svn/mod_authz_svn.c (working copy) @@ -63,6 +63,7 @@ typedef struct authz_svn_config_rec { const char *base_path; const char *access_file; const char *repo_relative_access_file; + const char *groups_file; const char *force_username_case; } authz_svn_config_rec; @@ -147,6 +148,16 @@ AuthzSVNReposRelativeAccessFile_cmd(cmd_parms *cmd return NULL; } +static const char * +AuthzSVNGroupsFile_cmd(cmd_parms *cmd, void *config, const char *arg1) +{ + authz_svn_config_rec *conf = config; + + conf->groups_file = canonicalize_access_file(arg1, TRUE, cmd->pool); + + return NULL; +} + /* Implements the #cmds member of Apache's #module vtable. */ static const command_rec authz_svn_cmds[] = { @@ -170,6 +181,14 @@ static const command_rec authz_svn_cmds[] = "file containing permissions of repository paths. Path may " "be an repository relative URL (^/) or absolute file:// URL " "to a text file in a Subversion repository."), + AP_INIT_TAKE1("AuthzSVNGroupsFile", + AuthzSVNGroupsFile_cmd, + NULL, + OR_AUTHCFG, + "Path to text file containing group definitions for all " + "repositories. Path may be an repository relative URL (^/) " + "or absolute file:// URL to a text file in a Subversion " + "repository."), AP_INIT_FLAG("AuthzSVNAnonymous", ap_set_flag_slot, (void *)APR_OFFSETOF(authz_svn_config_rec, anonymous), OR_AUTHCFG, @@ -331,6 +350,12 @@ get_access_conf(request_rec *r, authz_svn_config_r ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Path to authz file is %s", access_file); + if (conf->groups_file) + { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "Path to groups file is %s", conf->groups_file); + } + cache_key = apr_pstrcat(scratch_pool, "mod_authz_svn:", access_file, (char *)NULL); apr_pool_userdata_get(&user_data, cache_key, r->connection->pool); @@ -338,7 +363,8 @@ get_access_conf(request_rec *r, authz_svn_config_r if (access_conf == NULL) { svn_err = svn_repos_authz_read2(&access_conf, access_file, - TRUE, repos_path, r->connection->pool); + conf->groups_file, TRUE, repos_path, + r->connection->pool); if (svn_err) { log_svn_error(APLOG_MARK, r, Index: subversion/svnserve/serve.c =================================================================== --- subversion/svnserve/serve.c (revision 1437378) +++ subversion/svnserve/serve.c (working copy) @@ -269,35 +269,61 @@ svn_error_t *load_pwdb_config(server_baton_t *serv return SVN_NO_ERROR; } +/* Canonicalize ACCESS_FILE based on the type of argument. + * SERVER baton is used to convert relative paths to absolute paths + * rooted at the server root. */ +static const char * +canonicalize_access_file(const char *access_file, + server_baton_t *server, + apr_pool_t *pool) +{ + if (svn_path_is_url(access_file)) + { + access_file = svn_uri_canonicalize(access_file, pool); + } + else if (!svn_path_is_repos_relative_url(access_file)) + { + access_file = svn_dirent_internal_style(access_file, pool); + access_file = svn_dirent_join(server->base, access_file, pool); + } + + /* We don't canonicalize repos relative urls since they get + * canonicalized inside svn_repos_authz_read2() when they + * are resolved. */ + + return access_file; +} + svn_error_t *load_authz_config(server_baton_t *server, svn_ra_svn_conn_t *conn, const char *repos_root, apr_pool_t *pool) { const char *authzdb_path; + const char *groupsdb_path; svn_error_t *err; /* Read authz configuration. */ svn_config_get(server->cfg, &authzdb_path, SVN_CONFIG_SECTION_GENERAL, SVN_CONFIG_OPTION_AUTHZ_DB, NULL); + + svn_config_get(server->cfg, &groupsdb_path, SVN_CONFIG_SECTION_GENERAL, + SVN_CONFIG_OPTION_GROUPS_DB, NULL); + if (authzdb_path) { const char *case_force_val; - /* Canonicalize and add the base onto the authzdb_path (if needed). - * We don't canonicalize repos relative urls since they are - * canonicalized when they are resolved in svn_repos_authz_read2(). */ - if (svn_path_is_url(authzdb_path)) - { - authzdb_path = svn_uri_canonicalize(authzdb_path, pool); - } - else if (!svn_path_is_repos_relative_url(authzdb_path)) - { - authzdb_path = svn_dirent_internal_style(authzdb_path, pool); - authzdb_path = svn_dirent_join(server->base, authzdb_path, pool); - } - err = svn_repos_authz_read2(&server->authzdb, authzdb_path, TRUE, - repos_root, pool); + /* Canonicalize and add the base onto the authzdb_path (if needed). */ + authzdb_path = canonicalize_access_file(authzdb_path, server, pool); + + /* Same for the groupsdb_path if it is present. */ + if (groupsdb_path) + groupsdb_path = canonicalize_access_file(groupsdb_path, + server, pool); + + err = svn_repos_authz_read2(&server->authzdb, authzdb_path, + groupsdb_path, TRUE, repos_root, pool); if (err) { log_server_error(err, server, conn, pool); Index: subversion/tests/cmdline/authz_tests.py =================================================================== --- subversion/tests/cmdline/authz_tests.py (revision 1437378) +++ subversion/tests/cmdline/authz_tests.py (working copy) @@ -1452,6 +1452,69 @@ def remove_subdir_with_authz_and_tc(sbox): None, None, False, wc_dir) +@SkipUnless(svntest.main.is_ra_type_svn) +def authz_svnserve_groups(sbox): + "authz with configured global groups" + + sbox.build(create_wc = False) + + svntest.main.write_restrictive_svnserve_conf_with_groups(sbox.repo_dir) + + svntest.main.write_authz_file(sbox, { "/A/B" : "@senate = r", + "/A/D" : "@senate = rw", + "/A/B/E" : "@senate = " }) + + svntest.main.write_groups_file(sbox, { "senate" : "jrandom" }) + + root_url = sbox.repo_url + A_url = root_url + '/A' + B_url = A_url + '/B' + E_url = B_url + '/E' + F_url = B_url + '/F' + D_url = A_url + '/D' + G_url = D_url + '/G' + lambda_url = B_url + '/lambda' + pi_url = G_url + '/pi' + alpha_url = E_url + '/alpha' + + expected_err = ".*svn: E170001: Authorization failed.*" + + # read a remote file + svntest.actions.run_and_verify_svn(None, ["This is the file 'lambda'.\n"], + [], 'cat', + lambda_url) + + # read a remote file + svntest.actions.run_and_verify_svn(None, ["This is the file 'pi'.\n"], + [], 'cat', + pi_url) + + # read a remote file, unreadable: should fail + svntest.actions.run_and_verify_svn(None, + None, expected_err, + 'cat', + alpha_url) + + # copy a remote file, source is unreadable: should fail + svntest.actions.run_and_verify_svn(None, + None, expected_err, + 'cp', + '-m', 'logmsg', + alpha_url, B_url) + + # copy a remote folder + svntest.actions.run_and_verify_svn(None, None, [], + 'cp', + '-m', 'logmsg', + F_url, D_url) + + # copy a remote folder, source is unreadable: should fail + svntest.actions.run_and_verify_svn(None, + None, expected_err, + 'cp', + '-m', 'logmsg', + E_url, D_url) + ######################################################################## # Run the tests @@ -1481,7 +1544,8 @@ test_list = [ None, wc_delete, wc_commit_error_handling, upgrade_absent, - remove_subdir_with_authz_and_tc + remove_subdir_with_authz_and_tc, + authz_svnserve_groups ] serial_only = True Index: subversion/tests/cmdline/svntest/main.py =================================================================== --- subversion/tests/cmdline/svntest/main.py (revision 1437378) +++ subversion/tests/cmdline/svntest/main.py (working copy) @@ -1046,6 +1046,19 @@ def write_restrictive_svnserve_conf(repo_dir, anon fp.write("password-db = passwd\n") fp.close() +def write_restrictive_svnserve_conf_with_groups(repo_dir, + anon_access="none"): + "Create a restrictive configuration with groups stored in a separate file." + + fp = open(get_svnserve_conf_file_path(repo_dir), 'w') + fp.write("[general]\nanon-access = %s\nauth-access = write\n" + "authz-db = authz\ngroups-db = groups\n" % anon_access) + if options.enable_sasl: + fp.write("realm = svntest\n[sasl]\nuse-sasl = true\n"); + else: + fp.write("password-db = passwd\n") + fp.close() + # Warning: because mod_dav_svn uses one shared authz file for all # repositories, you *cannot* use write_authz_file in any test that # might be run in parallel. @@ -1081,6 +1094,18 @@ an appropriate list of mappings. fp.write("[%s%s]\n%s\n" % (prefix, p, r)) fp.close() +# See the warning about parallel test execution in write_authz_file +# method description. +def write_groups_file(sbox, groups): + """Write a groups file to SBOX, appropriate for the RA method used, +with group contents set to GROUPS.""" + fp = open(sbox.groups_file, 'w') + fp.write("[groups]\n") + if groups: + for p, r in groups.items(): + fp.write("%s = %s\n" % (p, r)) + fp.close() + def use_editor(func): os.environ['SVN_EDITOR'] = svneditor_script os.environ['SVN_MERGE'] = svneditor_script Index: subversion/tests/cmdline/svntest/sandbox.py =================================================================== --- subversion/tests/cmdline/svntest/sandbox.py (revision 1437378) +++ subversion/tests/cmdline/svntest/sandbox.py (working copy) @@ -78,12 +78,14 @@ class Sandbox: tmp_authz_file = os.path.join(svntest.main.work_dir, "authz-" + self.name) open(tmp_authz_file, 'w').write("[/]\n* = rw\n") shutil.move(tmp_authz_file, self.authz_file) + self.groups_file = os.path.join(svntest.main.work_dir, "groups") # For svnserve tests we have a per-repository authz file, and it # doesn't need to be there in order for things to work, so we don't # have any default contents. elif self.repo_url.startswith("svn"): self.authz_file = os.path.join(self.repo_dir, "conf", "authz") + self.groups_file = os.path.join(self.repo_dir, "conf", "groups") def clone_dependent(self, copy_wc=False): """A convenience method for creating a near-duplicate of this Index: subversion/tests/libsvn_repos/repos-test.c =================================================================== --- subversion/tests/libsvn_repos/repos-test.c (revision 1437378) +++ subversion/tests/libsvn_repos/repos-test.c (working copy) @@ -1157,7 +1157,7 @@ authz_get_handle(svn_authz_t **authz_p, const char SVN_ERR_W(svn_stream_puts(stream, authz_contents), "Writing authz contents to stream"); - SVN_ERR_W(svn_repos_authz_parse(authz_p, stream, pool), + SVN_ERR_W(svn_repos_authz_parse(authz_p, stream, NULL, pool), "Parsing the authz contents"); SVN_ERR_W(svn_stream_close(stream), @@ -1451,24 +1451,25 @@ in_repo_authz(const svn_test_opts_t *opts, /* repos relative URL */ repos_root = svn_repos_path(repos, pool); - SVN_ERR(svn_repos_authz_read2(&authz_cfg, "^/authz", TRUE, repos_root, - pool)); + SVN_ERR(svn_repos_authz_read2(&authz_cfg, "^/authz", NULL, TRUE, + repos_root, pool)); SVN_ERR(authz_check_access(authz_cfg, test_set, pool)); /* absolute file URL, repos_root is NULL to validate the contract that it * is not needed except when a repos relative URL is passed. */ SVN_ERR(svn_uri_get_file_url_from_dirent(&repos_url, repos_root, pool)); authz_url = apr_pstrcat(pool, repos_url, "/authz", (char *)NULL); - SVN_ERR(svn_repos_authz_read2(&authz_cfg, authz_url, TRUE, NULL, pool)); + SVN_ERR(svn_repos_authz_read2(&authz_cfg, authz_url, NULL, TRUE, + NULL, pool)); SVN_ERR(authz_check_access(authz_cfg, test_set, pool)); /* Non-existant path in the repo with must_exist set to FALSE */ - SVN_ERR(svn_repos_authz_read2(&authz_cfg, "^/A/authz", FALSE, repos_root, - pool)); + SVN_ERR(svn_repos_authz_read2(&authz_cfg, "^/A/authz", NULL, FALSE, + repos_root, pool)); /* Non-existant path in the repo with must_exist set to TRUE */ - err = svn_repos_authz_read2(&authz_cfg, "^/A/authz", TRUE, repos_root, - pool); + err = svn_repos_authz_read2(&authz_cfg, "^/A/authz", NULL, TRUE, + repos_root, pool); if (!err || err->apr_err != SVN_ERR_AUTHZ_INVALID_CONFIG) return svn_error_createf(SVN_ERR_TEST_FAILED, err, "Got %s error instead of expected " @@ -1478,7 +1479,7 @@ in_repo_authz(const svn_test_opts_t *opts, /* http:// URL which is unsupported */ err = svn_repos_authz_read2(&authz_cfg, "http://example.com/repo/authz", - TRUE, repos_root, pool); + NULL, TRUE, repos_root, pool); if (!err || err->apr_err != SVN_ERR_RA_ILLEGAL_URL) return svn_error_createf(SVN_ERR_TEST_FAILED, err, "Got %s error instead of expected " @@ -1488,6 +1489,149 @@ in_repo_authz(const svn_test_opts_t *opts, /* svn:// URL which is unsupported */ err = svn_repos_authz_read2(&authz_cfg, "svn://example.com/repo/authz", + NULL, TRUE, repos_root, pool); + if (!err || err->apr_err != SVN_ERR_RA_ILLEGAL_URL) + return svn_error_createf(SVN_ERR_TEST_FAILED, err, + "Got %s error instead of expected " + "SVN_ERR_RA_ILLEGAL_URL", + err ? "unexpected" : "no"); + svn_error_clear(err); + + + return SVN_NO_ERROR; +} + + +/* Test in-repo authz with global groups. */ +static svn_error_t * +in_repo_groups_authz(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_repos_t *repos; + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root; + svn_revnum_t youngest_rev; + svn_authz_t *authz_cfg; + const char *groups_contents; + const char *authz_contents; + const char *repos_root; + const char *repos_url; + const char *groups_url; + const char *authz_url; + svn_error_t *err; + struct check_access_tests test_set[] = { + /* reads */ + { "/A", NULL, NULL, svn_authz_read, FALSE }, + { "/A", NULL, "plato", svn_authz_read, TRUE }, + { "/A", NULL, "socrates", svn_authz_read, TRUE }, + { "/A", NULL, "solon", svn_authz_read, TRUE }, + { "/A", NULL, "ephialtes", svn_authz_read, TRUE }, + /* writes */ + { "/A", NULL, NULL, svn_authz_write, FALSE }, + { "/A", NULL, "plato", svn_authz_write, FALSE }, + { "/A", NULL, "socrates", svn_authz_write, FALSE }, + { "/A", NULL, "solon", svn_authz_write, TRUE }, + { "/A", NULL, "ephialtes", svn_authz_write, TRUE }, + /* Sentinel */ + { NULL, NULL, NULL, svn_authz_none, FALSE } + }; + + /* Test plan: + * 1. Create an authz file, a global groups file and an empty authz file, + * put all these files in the repository. The empty authz file is + * required to perform the non-existent path checks (4-7) -- + * otherwise we would get the authz validation error due to undefined + * groups. + * 2. Verify that the groups file can be read with an relative URL. + * 3. Verify that the groups file can be read with an absolute URL. + * 4. Verify that non-existent groups file path does not error out when + * must_exist is FALSE. + * 5. Same as (4), but when both authz and groups file paths do + * not exist. + * 6. Verify that non-existent path for the groups file does error out when + * must_exist is TRUE. + * 7. Verify that an http:// URL produces an error. + * 8. Verify that an svn:// URL produces an error. + */ + + /* What we'll put in the authz and groups files, it's simple since + * we're not testing the parsing, just that we got what we expected. */ + + groups_contents = + "[groups]" NL + "philosophers = plato, socrates" NL + "senate = solon, ephialtes" NL + "" NL; + + authz_contents = + "[/]" NL + "@senate = rw" NL + "@philosophers = r" NL + "" NL; + + /* Create a filesystem and repository. */ + SVN_ERR(svn_test__create_repos(&repos, + "test-repo-in-repo-global-groups-authz", + opts, pool)); + fs = svn_repos_fs(repos); + + /* Commit the authz, empty authz and groups files to the repo. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_file(txn_root, "groups", pool)); + SVN_ERR(svn_fs_make_file(txn_root, "authz", pool)); + SVN_ERR(svn_fs_make_file(txn_root, "empty-authz", pool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "groups", + groups_contents, pool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "authz", + authz_contents, pool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "empty-authz", "", pool)); + SVN_ERR(svn_repos_fs_commit_txn(NULL, repos, &youngest_rev, txn, pool)); + SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(youngest_rev)); + + /* repos relative URLs */ + repos_root = svn_repos_path(repos, pool); + SVN_ERR(svn_repos_authz_read2(&authz_cfg, "^/authz", "^/groups", + TRUE, repos_root, pool)); + SVN_ERR(authz_check_access(authz_cfg, test_set, pool)); + + /* absolute file URLs, repos_root is NULL to validate the contract that it + * is not needed except when a repos relative URLs are passed. */ + SVN_ERR(svn_uri_get_file_url_from_dirent(&repos_url, repos_root, pool)); + authz_url = apr_pstrcat(pool, repos_url, "/authz", (char *)NULL); + groups_url = apr_pstrcat(pool, repos_url, "/groups", (char *)NULL); + SVN_ERR(svn_repos_authz_read2(&authz_cfg, authz_url, groups_url, + TRUE, NULL, pool)); + SVN_ERR(authz_check_access(authz_cfg, test_set, pool)); + + /* Non-existent path for the groups file with must_exist + * set to TRUE */ + SVN_ERR(svn_repos_authz_read2(&authz_cfg, "^/empty-authz", + "^/A/groups", FALSE, + repos_root, pool)); + + /* Non-existent paths for both the authz and the groups files + * with must_exist set to TRUE */ + SVN_ERR(svn_repos_authz_read2(&authz_cfg, "^/A/authz", + "^/A/groups", FALSE, + repos_root, pool)); + + /* Non-existent path for the groups file with must_exist + * set to TRUE */ + err = svn_repos_authz_read2(&authz_cfg, "^/empty-authz", + "^/A/groups", TRUE, + repos_root, pool); + if (!err || err->apr_err != SVN_ERR_AUTHZ_INVALID_CONFIG) + return svn_error_createf(SVN_ERR_TEST_FAILED, err, + "Got %s error instead of expected " + "SVN_ERR_AUTHZ_INVALID_CONFIG", + err ? "unexpected" : "no"); + svn_error_clear(err); + + /* http:// URL which is unsupported */ + err = svn_repos_authz_read2(&authz_cfg, "^/empty-authz", + "http://example.com/repo/groups", TRUE, repos_root, pool); if (!err || err->apr_err != SVN_ERR_RA_ILLEGAL_URL) return svn_error_createf(SVN_ERR_TEST_FAILED, err, @@ -1496,10 +1640,264 @@ in_repo_authz(const svn_test_opts_t *opts, err ? "unexpected" : "no"); svn_error_clear(err); + /* svn:// URL which is unsupported */ + err = svn_repos_authz_read2(&authz_cfg, "^/empty-authz", + "http://example.com/repo/groups", + TRUE, repos_root, pool); + if (!err || err->apr_err != SVN_ERR_RA_ILLEGAL_URL) + return svn_error_createf(SVN_ERR_TEST_FAILED, err, + "Got %s error instead of expected " + "SVN_ERR_RA_ILLEGAL_URL", + err ? "unexpected" : "no"); + svn_error_clear(err); + return SVN_NO_ERROR; } + +/* Helper for the groups_authz test. Set *AUTHZ_P to a representation of + AUTHZ_CONTENTS in conjuction with GROUPS_CONTENTS, using POOL for + temporary allocation. If DISK is TRUE then write the contents to + temporary files and use svn_repos_authz_read2() to get the data if FALSE + write the data to a buffered stream and use svn_repos_authz_parse(). */ +static svn_error_t * +authz_groups_get_handle(svn_authz_t **authz_p, + const char *authz_contents, + const char *groups_contents, + svn_boolean_t disk, + apr_pool_t *pool) +{ + if (disk) + { + const char *authz_file_path; + const char *groups_file_path; + + /* Create temporary files. */ + SVN_ERR_W(svn_io_write_unique(&authz_file_path, NULL, + authz_contents, + strlen(authz_contents), + svn_io_file_del_on_pool_cleanup, pool), + "Writing temporary authz file"); + SVN_ERR_W(svn_io_write_unique(&groups_file_path, NULL, + groups_contents, + strlen(groups_contents), + svn_io_file_del_on_pool_cleanup, pool), + "Writing temporary groups file"); + + /* Read the authz configuration back and start testing. */ + SVN_ERR_W(svn_repos_authz_read2(authz_p, authz_file_path, + groups_file_path, TRUE, NULL, pool), + "Opening test authz and groups files"); + + /* Done with the files. */ + SVN_ERR_W(svn_io_remove_file(authz_file_path, pool), + "Removing test authz file"); + SVN_ERR_W(svn_io_remove_file(groups_file_path, pool), + "Removing test groups file"); + } + else + { + svn_stream_t *stream; + svn_stream_t *groups_stream; + + /* Create the streams. */ + stream = svn_stream_buffered(pool); + groups_stream = svn_stream_buffered(pool); + + SVN_ERR_W(svn_stream_puts(stream, authz_contents), + "Writing authz contents to stream"); + SVN_ERR_W(svn_stream_puts(groups_stream, groups_contents), + "Writing groups contents to stream"); + + /* Read the authz configuration from the streams and start testing. */ + SVN_ERR_W(svn_repos_authz_parse(authz_p, stream, groups_stream, pool), + "Parsing the authz and groups contents"); + + /* Done with the streams. */ + SVN_ERR_W(svn_stream_close(stream), + "Closing the authz stream"); + SVN_ERR_W(svn_stream_close(groups_stream), + "Closing the groups stream"); + } + + return SVN_NO_ERROR; +} + +/* Test authz with global groups. */ +static svn_error_t * +groups_authz(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_authz_t *authz_cfg; + const char *authz_contents; + const char *groups_contents; + svn_error_t *err; + + struct check_access_tests test_set1[] = { + /* reads */ + { "/A", "greek", NULL, svn_authz_read, FALSE }, + { "/A", "greek", "plato", svn_authz_read, TRUE }, + { "/A", "greek", "demetrius", svn_authz_read, TRUE }, + { "/A", "greek", "galenos", svn_authz_read, TRUE }, + { "/A", "greek", "pamphilos", svn_authz_read, FALSE }, + /* writes */ + { "/A", "greek", NULL, svn_authz_write, FALSE }, + { "/A", "greek", "plato", svn_authz_write, TRUE }, + { "/A", "greek", "demetrius", svn_authz_write, FALSE }, + { "/A", "greek", "galenos", svn_authz_write, FALSE }, + { "/A", "greek", "pamphilos", svn_authz_write, FALSE }, + /* Sentinel */ + { NULL, NULL, NULL, svn_authz_none, FALSE } + }; + + struct check_access_tests test_set2[] = { + /* reads */ + { "/A", "greek", NULL, svn_authz_read, FALSE }, + { "/A", "greek", "socrates", svn_authz_read, FALSE }, + { "/B", "greek", NULL, svn_authz_read, FALSE}, + { "/B", "greek", "socrates", svn_authz_read, TRUE }, + /* writes */ + { "/A", "greek", NULL, svn_authz_write, FALSE }, + { "/A", "greek", "socrates", svn_authz_write, FALSE }, + { "/B", "greek", NULL, svn_authz_write, FALSE}, + { "/B", "greek", "socrates", svn_authz_write, TRUE }, + /* Sentinel */ + { NULL, NULL, NULL, svn_authz_none, FALSE } + }; + + /* Test plan: + * 1. Ensure that a simple setup with global groups and access rights in + * two separate files works as expected. + * 2. Verify that access rights written in the global groups file are + * discarded and affect nothing in authorization terms. + * 3. Verify that local groups in the authz file are prohibited in + * conjuction with global groups (and that a configuration error is + * reported in this scenario). + * 4. Ensure that group cycles in the global groups file are reported. + * + * All checks are performed twice -- for the configurations stored on disk + * and in memory. See authz_groups_get_handle. + */ + + groups_contents = + "[groups]" NL + "slaves = pamphilos,@gladiators" NL + "gladiators = demetrius,galenos" NL + "philosophers = plato" NL + "" NL; + + authz_contents = + "[greek:/A]" NL + "@slaves = " NL + "@gladiators = r" NL + "@philosophers = rw" NL + "" NL; + + SVN_ERR(authz_groups_get_handle(&authz_cfg, authz_contents, + groups_contents, TRUE, pool)); + + SVN_ERR(authz_check_access(authz_cfg, test_set1, pool)); + + SVN_ERR(authz_groups_get_handle(&authz_cfg, authz_contents, + groups_contents, FALSE, pool)); + + SVN_ERR(authz_check_access(authz_cfg, test_set1, pool)); + + /* Access rights in the global groups file are discarded. */ + groups_contents = + "[groups]" NL + "philosophers = socrates" NL + "" NL + "[greek:/A]" NL + "@philosophers = rw" NL + "" NL; + + authz_contents = + "[greek:/B]" NL + "@philosophers = rw" NL + "" NL; + + SVN_ERR(authz_groups_get_handle(&authz_cfg, authz_contents, + groups_contents, TRUE, pool)); + + SVN_ERR(authz_check_access(authz_cfg, test_set2, pool)); + + SVN_ERR(authz_groups_get_handle(&authz_cfg, authz_contents, + groups_contents, FALSE, pool)); + + SVN_ERR(authz_check_access(authz_cfg, test_set2, pool)); + + /* Local groups cannot be used in conjuction with global groups. */ + groups_contents = + "[groups]" NL + "slaves = maximus" NL + "" NL; + + authz_contents = + "[greek:/A]" NL + "@slaves = " NL + "@kings = rw" NL + "" NL + "[groups]" NL + /* That's an epic story of the slave who tried to become a king. */ + "kings = maximus" NL + "" NL; + + err = authz_groups_get_handle(&authz_cfg, authz_contents, + groups_contents, TRUE, pool); + + if (!err || err->apr_err != SVN_ERR_AUTHZ_INVALID_CONFIG) + return svn_error_createf(SVN_ERR_TEST_FAILED, err, + "Got %s error instead of expected " + "SVN_ERR_AUTHZ_INVALID_CONFIG", + err ? "unexpected" : "no"); + svn_error_clear(err); + + err = authz_groups_get_handle(&authz_cfg, authz_contents, + groups_contents, FALSE, pool); + + if (!err || err->apr_err != SVN_ERR_AUTHZ_INVALID_CONFIG) + return svn_error_createf(SVN_ERR_TEST_FAILED, err, + "Got %s error instead of expected " + "SVN_ERR_AUTHZ_INVALID_CONFIG", + err ? "unexpected" : "no"); + svn_error_clear(err); + + /* Ensure that group cycles are reported. */ + groups_contents = + "[groups]" NL + "slaves = cooks,scribes,@gladiators" NL + "gladiators = equites,thraces,@slaves" NL + "" NL; + + authz_contents = + "[greek:/A]" NL + "@slaves = r" NL + "" NL; + + err = authz_groups_get_handle(&authz_cfg, authz_contents, + groups_contents, TRUE, pool); + + if (!err || err->apr_err != SVN_ERR_AUTHZ_INVALID_CONFIG) + return svn_error_createf(SVN_ERR_TEST_FAILED, err, + "Got %s error instead of expected " + "SVN_ERR_AUTHZ_INVALID_CONFIG", + err ? "unexpected" : "no"); + svn_error_clear(err); + + err = authz_groups_get_handle(&authz_cfg, authz_contents, + groups_contents, FALSE, pool); + + if (!err || err->apr_err != SVN_ERR_AUTHZ_INVALID_CONFIG) + return svn_error_createf(SVN_ERR_TEST_FAILED, err, + "Got %s error instead of expected " + "SVN_ERR_AUTHZ_INVALID_CONFIG", + err ? "unexpected" : "no"); + svn_error_clear(err); + + return SVN_NO_ERROR; +} /* Callback for the commit editor tests that relays requests to authz. */ @@ -2745,6 +3143,10 @@ struct svn_test_descriptor_t test_funcs[] = "test authz access control"), SVN_TEST_OPTS_PASS(in_repo_authz, "test authz stored in the repo"), + SVN_TEST_OPTS_PASS(in_repo_groups_authz, + "test authz and global groups stored in the repo"), + SVN_TEST_OPTS_PASS(groups_authz, + "test authz with global groups"), SVN_TEST_OPTS_PASS(commit_editor_authz, "test authz in the commit editor"), SVN_TEST_OPTS_PASS(commit_continue_txn, Index: tools/server-side/svnauthz.c =================================================================== --- tools/server-side/svnauthz.c (revision 1437378) +++ tools/server-side/svnauthz.c (working copy) @@ -39,7 +39,8 @@ enum svnauthz__cmdline_options_t svnauthz__username, svnauthz__path, svnauthz__repos, - svnauthz__is + svnauthz__is, + svnauthz__groups_file }; /* Option codes and descriptions. @@ -66,6 +67,7 @@ static const apr_getopt_option_t options_table[] = " " " no no access\n") }, + {"groups-file", svnauthz__groups_file, 1, ("path to the global groups file")}, {0, 0, 0, 0} }; @@ -74,6 +76,7 @@ struct svnauthz_opt_state svn_boolean_t help; svn_boolean_t version; const char *authz_file; + const char *groups_file; const char *username; const char *fspath; const char *repos_name; @@ -120,8 +123,9 @@ static const svn_opt_subcommand_desc2_t cmd_table[ {'t'} }, {"accessof", subcommand_accessof, {0} /* no aliases */, ("Print or test the permissions set by an authz file for a specific circumstance.\n" - "usage: 1. svnauthz accessof [--username USER] TARGET\n" - " 2. svnauthz accessof [--username USER] -t TXN REPOS_PATH FILE_PATH\n\n" + "usage: 1. svnauthz accessof [--username USER] [--groups-file GROUPS_FILE] TARGET\n" + " 2. svnauthz accessof [--username USER] [--groups-file GROUPS_FILE] \\\n" + " -t TXN REPOS_PATH FILE_PATH\n\n" " 1. Prints the access of USER based on TARGET.\n" " TARGET can be a path to a file or an absolute file:// URL to an authz\n" " file in a repository, but cannot be a repository relative URL (^/).\n\n" @@ -129,7 +133,8 @@ static const svn_opt_subcommand_desc2_t cmd_table[ " transaction TXN in the repository at REPOS_PATH.\n\n" " If the --username argument is omitted then access of an anonymous user\n" " will be printed. If --path argument is omitted prints if any access\n" - " to the repo is allowed.\n\n" + " to the repo is allowed. If --groups-file is specified, the groups from\n" + " GROUPS_FILE will be used.\n\n" "Outputs one of the following:\n" " rw write access (which also implies read)\n" " r read access\n" @@ -140,7 +145,8 @@ static const svn_opt_subcommand_desc2_t cmd_table[ " 2 operational error\n" " 3 when --is argument doesn't match\n" ), - {'t', svnauthz__username, svnauthz__path, svnauthz__repos, svnauthz__is} }, + {'t', svnauthz__username, svnauthz__path, svnauthz__repos, svnauthz__is, + svnauthz__groups_file} }, { NULL, NULL, {0}, NULL, {0} } }; @@ -176,20 +182,40 @@ subcommand_help(apr_getopt_t *os, void *baton, apr return SVN_NO_ERROR; } +/* Loads the fs FILENAME contents into *CONTENTS ensuring that the + corresponding node is a file. Using POOL for allocations. */ +static svn_error_t * +read_file_contents(svn_stream_t **contents, const char *filename, + svn_fs_root_t *root, apr_pool_t *pool) +{ + svn_node_kind_t node_kind; + + /* Make sure the path is a file */ + SVN_ERR(svn_fs_check_path(&node_kind, root, filename, pool)); + if (node_kind != svn_node_file) + return svn_error_createf(SVN_ERR_FS_NOT_FILE, NULL, + "Path '%s' is not a file", filename); + + SVN_ERR(svn_fs_file_contents(contents, root, filename, pool)); + + return SVN_NO_ERROR; +} + /* Loads the authz config into *AUTHZ from the file at AUTHZ_FILE - in repository at REPOS_PATH from the transaction TXN_NAME. Using - POOL for allocations. */ + in repository at REPOS_PATH from the transaction TXN_NAME. If GROUPS_FILE + is set, the resulting *AUTHZ will be constructed from AUTHZ_FILE with + global groups taken from GROUPS_FILE. Using POOL for allocations. */ static svn_error_t * get_authz_from_txn(svn_authz_t **authz, const char *repos_path, - const char *authz_file, const char *txn_name, - apr_pool_t *pool) + const char *authz_file, const char *groups_file, + const char *txn_name, apr_pool_t *pool) { svn_repos_t *repos; svn_fs_t *fs; svn_fs_txn_t *txn; svn_fs_root_t *root; - svn_node_kind_t node_kind; - svn_stream_t *contents; + svn_stream_t *authz_contents; + svn_stream_t *groups_contents; svn_error_t *err; /* Open up the repository and find the transaction root */ @@ -198,15 +224,17 @@ get_authz_from_txn(svn_authz_t **authz, const char SVN_ERR(svn_fs_open_txn(&txn, fs, txn_name, pool)); SVN_ERR(svn_fs_txn_root(&root, txn, pool)); - /* Make sure the path is a file */ - SVN_ERR(svn_fs_check_path(&node_kind, root, authz_file, pool)); - if (node_kind != svn_node_file) - return svn_error_createf(SVN_ERR_FS_NOT_FILE, NULL, - "Path '%s' is not a file", authz_file); + /* Get the authz file contents. */ + SVN_ERR(read_file_contents(&authz_contents, authz_file, root, pool)); - SVN_ERR(svn_fs_file_contents(&contents, root, authz_file, pool)); - err = svn_repos_authz_parse(authz, contents, pool); + /* Get the groups file contents if needed. */ + if (groups_file) + SVN_ERR(read_file_contents(&groups_contents, groups_file, root, pool)); + else + groups_contents = NULL; + err = svn_repos_authz_parse(authz, authz_contents, groups_contents, pool); + /* Add the filename to the error stack since the parser doesn't have it. */ if (err != SVN_NO_ERROR) return svn_error_createf(err->apr_err, err, @@ -216,8 +244,10 @@ get_authz_from_txn(svn_authz_t **authz, const char } /* Loads the authz config into *AUTHZ from OPT_STATE->AUTHZ_FILE. If - OPT_STATE->TXN is set then OPT_STATE->AUTHZ_FILE is treated as a fspath - in repository at OPT_STATE->REPOS_PATH. */ + OPT_STATE->GROUPS_FILE is set, loads the global groups from it. + If OPT_STATE->TXN is set then OPT_STATE->AUTHZ_FILE and + OPT_STATE->GROUPS_FILE are treated as fspaths in repository at + OPT_STATE->REPOS_PATH. */ static svn_error_t * get_authz(svn_authz_t **authz, struct svnauthz_opt_state *opt_state, apr_pool_t *pool) @@ -225,10 +255,14 @@ get_authz(svn_authz_t **authz, struct svnauthz_opt /* Read the access file and validate it. */ if (opt_state->txn) return get_authz_from_txn(authz, opt_state->repos_path, - opt_state->authz_file, opt_state->txn, pool); + opt_state->authz_file, + opt_state->groups_file, + opt_state->txn, pool); /* Else */ - return svn_repos_authz_read2(authz, opt_state->authz_file, TRUE, NULL, pool); + return svn_repos_authz_read2(authz, opt_state->authz_file, + opt_state->groups_file, + TRUE, NULL, pool); } static svn_error_t * @@ -368,6 +402,55 @@ use_compat_mode(const char *cmd, apr_pool_t *pool) sizeof(SVNAUTHZ_COMPAT_NAME)-1); } +/* Canonicalize ACCESS_FILE into *CANONICALIZED_ACCESS_FILE based on the type + of argument. Error out on unsupported path types. If WITHIN_TXN is set, + ACCESS_FILE has to be a fspath in the repo. Use POOL for allocations. */ +static svn_error_t * +canonicalize_access_file(const char **canonicalized_access_file, + const char *access_file, + svn_boolean_t within_txn, + apr_pool_t *pool) +{ + if (svn_path_is_repos_relative_url(access_file)) + { + /* Can't accept repos relative urls since we don't have the path to + * the repository. */ + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + ("'%s' is a repository relative URL when it " + "should be a local path or file:// URL"), + access_file); + } + else if (svn_path_is_url(access_file)) + { + if (within_txn) + { + /* Don't allow urls with transaction argument. */ + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + ("'%s' is a URL when it should be a " + "repository-relative path"), + access_file); + } + + *canonicalized_access_file = svn_uri_canonicalize(access_file, pool); + } + else if (within_txn) + { + /* Transaction flag means this has to be a fspath to the access file + * in the repo. */ + *canonicalized_access_file = + svn_fspath__canonicalize(access_file, pool); + } + else + { + /* If it isn't a URL and there's no transaction flag then it's a + * dirent to the access file on local disk. */ + *canonicalized_access_file = + svn_dirent_internal_style(access_file, pool); + } + + return SVN_NO_ERROR; +} + static int sub_main(int argc, const char *argv[], apr_pool_t *pool) { @@ -386,7 +469,7 @@ sub_main(int argc, const char *argv[], apr_pool_t /* Initialize opt_state */ opt_state.username = opt_state.fspath = opt_state.repos_name = NULL; - opt_state.txn = opt_state.repos_path = NULL; + opt_state.txn = opt_state.repos_path = opt_state.groups_file = NULL; /* Parse options. */ SVN_INT_ERR(svn_cmdline__getopt_init(&os, argc, argv, pool)); @@ -437,6 +520,11 @@ sub_main(int argc, const char *argv[], apr_pool_t case svnauthz__is: SVN_INT_ERR(svn_utf_cstring_to_utf8(&opt_state.is, arg, pool)); break; + case svnauthz__groups_file: + SVN_INT_ERR( + svn_utf_cstring_to_utf8(&opt_state.groups_file, + arg, pool)); + break; default: { SVN_INT_ERR(subcommand_help(NULL, NULL, pool)); @@ -537,47 +625,18 @@ sub_main(int argc, const char *argv[], apr_pool_t SVN_INT_ERR(svn_utf_cstring_to_utf8(&opt_state.authz_file, os->argv[os->ind], pool)); - /* Canonicalize opt_state.authz_file appropriately */ - if (svn_path_is_repos_relative_url(opt_state.authz_file)) - { - /* Can't accept repos relative urls since we don't have the path to - * the repository. */ - err = svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, - ("'%s' is a repository relative URL when it " - "should be a local path or file:// URL"), - opt_state.authz_file); - return EXIT_ERROR(err, EXIT_FAILURE); - } - else if (svn_path_is_url(opt_state.authz_file)) - { - if (opt_state.txn) - { - /* don't allow urls with transaction argument */ - err = svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, - ("'%s' is a URL when it should be a " - "repository-relative path"), - opt_state.authz_file); - return EXIT_ERROR(err, EXIT_FAILURE); - } + /* Canonicalize opt_state.authz_file appropriately. */ + SVN_INT_ERR(canonicalize_access_file(&opt_state.authz_file, + opt_state.authz_file, + opt_state.txn != NULL, pool)); - opt_state.authz_file = svn_uri_canonicalize(opt_state.authz_file, - pool); - } - else if (opt_state.txn) + /* Same for opt_state.groups_file if it is present. */ + if (opt_state.groups_file) { - /* Transaction flag means this has to be a fspath to the authz_file - * in the repo. */ - opt_state.authz_file = - svn_fspath__canonicalize(opt_state.authz_file, pool); + SVN_INT_ERR(canonicalize_access_file(&opt_state.groups_file, + opt_state.groups_file, + opt_state.txn != NULL, pool)); } - else - { - /* If it isn't a URL and there's no transaction flag then it's a - * dirent to a authz_file on local disk. */ - opt_state.authz_file = svn_dirent_internal_style(opt_state.authz_file, - pool); - } - } /* Check that the subcommand wasn't passed any inappropriate options. */