| /* |
| * mod_authz_svn.c: an Apache mod_dav_svn sub-module to provide path |
| * based authorization for a Subversion repository. |
| * |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| */ |
| |
| |
| |
| #include <httpd.h> |
| #include <http_config.h> |
| #include <http_core.h> |
| #include <http_request.h> |
| #include <http_protocol.h> |
| #include <http_log.h> |
| #include <http_config.h> |
| #include <ap_config.h> |
| #include <ap_provider.h> |
| #include <ap_mmn.h> |
| #include <apr_uri.h> |
| #include <apr_lib.h> |
| #include <mod_dav.h> |
| |
| #include "mod_dav_svn.h" |
| #include "mod_authz_svn.h" |
| #include "svn_path.h" |
| #include "svn_config.h" |
| #include "svn_string.h" |
| #include "svn_repos.h" |
| #include "svn_pools.h" |
| #include "svn_dirent_uri.h" |
| #include "private/svn_fspath.h" |
| |
| /* The apache headers define these and they conflict with our definitions. */ |
| #ifdef PACKAGE_BUGREPORT |
| #undef PACKAGE_BUGREPORT |
| #endif |
| #ifdef PACKAGE_NAME |
| #undef PACKAGE_NAME |
| #endif |
| #ifdef PACKAGE_STRING |
| #undef PACKAGE_STRING |
| #endif |
| #ifdef PACKAGE_TARNAME |
| #undef PACKAGE_TARNAME |
| #endif |
| #ifdef PACKAGE_VERSION |
| #undef PACKAGE_VERSION |
| #endif |
| #include "svn_private_config.h" |
| |
| #ifdef APLOG_USE_MODULE |
| APLOG_USE_MODULE(authz_svn); |
| #else |
| /* This is part of the APLOG_USE_MODULE() macro in httpd-2.3 */ |
| extern module AP_MODULE_DECLARE_DATA authz_svn_module; |
| #endif |
| |
| typedef struct authz_svn_config_rec { |
| int authoritative; |
| int anonymous; |
| int no_auth_when_anon_ok; |
| 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; |
| |
| #if AP_MODULE_MAGIC_AT_LEAST(20060110,0) /* version where |
| ap_some_auth_required breaks */ |
| # if AP_MODULE_MAGIC_AT_LEAST(20120211,47) /* first version with |
| force_authn hook and |
| ap_some_authn_required() which |
| allows us to work without |
| ap_some_auth_required() */ |
| # define USE_FORCE_AUTHN 1 |
| # define IN_SOME_AUTHN_NOTE "authz_svn-in-some-authn" |
| # define FORCE_AUTHN_NOTE "authz_svn-force-authn" |
| # else |
| /* ap_some_auth_required() is busted and no viable alternative exists */ |
| # ifndef SVN_ALLOW_BROKEN_HTTPD_AUTH |
| # error This version of httpd has a security hole with mod_authz_svn |
| # else |
| /* user wants to build anyway */ |
| # define USE_FORCE_AUTHN 0 |
| # endif |
| # endif |
| #else |
| /* old enough that ap_some_auth_required() still works */ |
| # define USE_FORCE_AUTHN 0 |
| #endif |
| |
| /* |
| * Configuration |
| */ |
| |
| /* Implements the #create_dir_config method of Apache's #module vtable. */ |
| static void * |
| create_authz_svn_dir_config(apr_pool_t *p, char *d) |
| { |
| authz_svn_config_rec *conf = apr_pcalloc(p, sizeof(*conf)); |
| conf->base_path = d; |
| |
| if (d) |
| conf->base_path = svn_urlpath__canonicalize(d, p); |
| |
| /* By default keep the fortress secure */ |
| conf->authoritative = 1; |
| conf->anonymous = 1; |
| |
| return conf; |
| } |
| |
| /* canonicalize ACCESS_FILE based on the type of argument. |
| * If SERVER_RELATIVE is true, ACCESS_FILE is a relative |
| * path then ACCESS_FILE is converted to an absolute |
| * path rooted at the server root. |
| * Returns NULL if path is not valid.*/ |
| static const char * |
| canonicalize_access_file(const char *access_file, |
| svn_boolean_t server_relative, |
| 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)) |
| { |
| if (server_relative) |
| { |
| access_file = ap_server_root_relative(pool, access_file); |
| if (access_file == NULL) |
| return NULL; |
| } |
| |
| access_file = svn_dirent_internal_style(access_file, pool); |
| } |
| |
| /* We don't canonicalize repos relative urls since they get |
| * canonicalized before calling svn_repos_authz_read2() when they |
| * are resolved. */ |
| |
| return access_file; |
| } |
| |
| static const char * |
| AuthzSVNAccessFile_cmd(cmd_parms *cmd, void *config, const char *arg1) |
| { |
| authz_svn_config_rec *conf = config; |
| |
| if (conf->repo_relative_access_file != NULL) |
| return "AuthzSVNAccessFile and AuthzSVNReposRelativeAccessFile " |
| "directives are mutually exclusive."; |
| |
| conf->access_file = canonicalize_access_file(arg1, TRUE, cmd->pool); |
| if (!conf->access_file) |
| return apr_pstrcat(cmd->pool, "Invalid file path ", arg1, SVN_VA_NULL); |
| |
| return NULL; |
| } |
| |
| |
| static const char * |
| AuthzSVNReposRelativeAccessFile_cmd(cmd_parms *cmd, |
| void *config, |
| const char *arg1) |
| { |
| authz_svn_config_rec *conf = config; |
| |
| if (conf->access_file != NULL) |
| return "AuthzSVNAccessFile and AuthzSVNReposRelativeAccessFile " |
| "directives are mutually exclusive."; |
| |
| conf->repo_relative_access_file = canonicalize_access_file(arg1, FALSE, |
| cmd->pool); |
| |
| if (!conf->repo_relative_access_file) |
| return apr_pstrcat(cmd->pool, "Invalid file path ", arg1, SVN_VA_NULL); |
| |
| 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); |
| |
| if (!conf->groups_file) |
| return apr_pstrcat(cmd->pool, "Invalid file path ", arg1, SVN_VA_NULL); |
| |
| return NULL; |
| } |
| |
| /* Implements the #cmds member of Apache's #module vtable. */ |
| static const command_rec authz_svn_cmds[] = |
| { |
| AP_INIT_FLAG("AuthzSVNAuthoritative", ap_set_flag_slot, |
| (void *)APR_OFFSETOF(authz_svn_config_rec, authoritative), |
| OR_AUTHCFG, |
| "Set to 'Off' to allow access control to be passed along to " |
| "lower modules. (default is On.)"), |
| AP_INIT_TAKE1("AuthzSVNAccessFile", AuthzSVNAccessFile_cmd, |
| NULL, |
| OR_AUTHCFG, |
| "Path to text 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("AuthzSVNReposRelativeAccessFile", |
| AuthzSVNReposRelativeAccessFile_cmd, |
| NULL, |
| OR_AUTHCFG, |
| "Path (relative to repository 'conf' directory) to text " |
| "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, |
| "Set to 'Off' to disable two special-case behaviours of " |
| "this module: (1) interaction with the 'Satisfy Any' " |
| "directive, and (2) enforcement of the authorization " |
| "policy even when no 'Require' directives are present. " |
| "(default is On.)"), |
| AP_INIT_FLAG("AuthzSVNNoAuthWhenAnonymousAllowed", ap_set_flag_slot, |
| (void *)APR_OFFSETOF(authz_svn_config_rec, |
| no_auth_when_anon_ok), |
| OR_AUTHCFG, |
| "Set to 'On' to suppress authentication and authorization " |
| "for requests which anonymous users are allowed to perform. " |
| "(default is Off.)"), |
| AP_INIT_TAKE1("AuthzForceUsernameCase", ap_set_string_slot, |
| (void *)APR_OFFSETOF(authz_svn_config_rec, |
| force_username_case), |
| OR_AUTHCFG, |
| "Set to 'Upper' or 'Lower' to convert the username before " |
| "checking for authorization."), |
| { NULL } |
| }; |
| |
| |
| /* The macros LOG_ARGS_SIGNATURE and LOG_ARGS_CASCADE are expanded as formal |
| * and actual parameters to log_access_verdict with respect to HTTPD version. |
| */ |
| #if AP_MODULE_MAGIC_AT_LEAST(20100606,0) |
| #define LOG_ARGS_SIGNATURE const char *file, int line, int module_index |
| #define LOG_ARGS_CASCADE file, line, module_index |
| #else |
| #define LOG_ARGS_SIGNATURE const char *file, int line |
| #define LOG_ARGS_CASCADE file, line |
| #endif |
| |
| /* Log a message indicating the access control decision made about a |
| * request. The macro LOG_ARGS_SIGNATURE expands to FILE, LINE and |
| * MODULE_INDEX in HTTPD 2.3 as APLOG_MARK macro has been changed for |
| * per-module loglevel configuration. It expands to FILE and LINE |
| * in older server versions. ALLOWED is boolean. |
| * REPOS_PATH and DEST_REPOS_PATH are information |
| * about the request. DEST_REPOS_PATH may be NULL. |
| * Non-zero IS_SUBREQ_BYPASS means that this authorization check was |
| * implicitly requested using 'subrequest bypass' callback from |
| * mod_dav_svn. |
| */ |
| static void |
| log_access_verdict(LOG_ARGS_SIGNATURE, |
| const request_rec *r, int allowed, int is_subreq_bypass, |
| const char *repos_path, const char *dest_repos_path) |
| { |
| int level = allowed ? APLOG_INFO : APLOG_ERR; |
| const char *verdict = allowed ? "granted" : "denied"; |
| |
| /* Use less important log level for implicit sub-request authorization |
| checks. */ |
| if (is_subreq_bypass) |
| level = APLOG_INFO; |
| else if (r->main && r->method_number == M_GET) |
| level = APLOG_INFO; |
| |
| if (r->user) |
| { |
| if (dest_repos_path) |
| ap_log_rerror(LOG_ARGS_CASCADE, level, 0, r, |
| "Access %s: '%s' %s %s %s", verdict, r->user, |
| r->method, repos_path, dest_repos_path); |
| else |
| ap_log_rerror(LOG_ARGS_CASCADE, level, 0, r, |
| "Access %s: '%s' %s %s", verdict, r->user, |
| r->method, repos_path); |
| } |
| else |
| { |
| if (dest_repos_path) |
| ap_log_rerror(LOG_ARGS_CASCADE, level, 0, r, |
| "Access %s: - %s %s %s", verdict, |
| r->method, repos_path, dest_repos_path); |
| else |
| ap_log_rerror(LOG_ARGS_CASCADE, level, 0, r, |
| "Access %s: - %s %s", verdict, |
| r->method, repos_path); |
| } |
| } |
| |
| /* Log a message indiciating the ERR encountered during the request R. |
| * LOG_ARGS_SIGNATURE expands as in log_access_verdict() above. |
| * PREFIX is inserted at the start of the message. The rest of the |
| * message is generated by combining the message for each error in the |
| * chain of ERR, excluding for trace errors. ERR will be cleared |
| * when finished. */ |
| static void |
| log_svn_error(LOG_ARGS_SIGNATURE, |
| request_rec *r, const char *prefix, |
| svn_error_t *err, apr_pool_t *scratch_pool) |
| { |
| svn_error_t *err_pos = svn_error_purge_tracing(err); |
| svn_stringbuf_t *buff = svn_stringbuf_create(prefix, scratch_pool); |
| |
| /* Build the error chain into a space separated stringbuf. */ |
| while (err_pos) |
| { |
| svn_stringbuf_appendbyte(buff, ' '); |
| if (err_pos->message) |
| { |
| svn_stringbuf_appendcstr(buff, err_pos->message); |
| } |
| else |
| { |
| char strerr[256]; |
| |
| svn_stringbuf_appendcstr(buff, svn_strerror(err->apr_err, strerr, |
| sizeof(strerr))); |
| } |
| |
| err_pos = err_pos->child; |
| } |
| |
| ap_log_rerror(LOG_ARGS_CASCADE, APLOG_ERR, |
| /* If it is an error code that APR can make sense of, then |
| show it, otherwise, pass zero to avoid putting "APR does |
| not understand this error code" in the error log. */ |
| ((err->apr_err >= APR_OS_START_USERERR && |
| err->apr_err < APR_OS_START_CANONERR) ? |
| 0 : err->apr_err), |
| r, "%s", buff->data); |
| |
| svn_error_clear(err); |
| } |
| |
| /* Resolve *PATH into an absolute canonical URL iff *PATH is a repos-relative |
| * URL. If *REPOS_URL is NULL convert REPOS_PATH into a file URL stored |
| * in *REPOS_URL, if *REPOS_URL is not null REPOS_PATH is ignored. The |
| * resulting *REPOS_URL will be used as the root of the repos-relative URL. |
| * The result will be stored in *PATH. */ |
| static svn_error_t * |
| resolve_repos_relative_url(const char **path, const char **repos_url, |
| const char *repos_path, apr_pool_t *pool) |
| { |
| if (svn_path_is_repos_relative_url(*path)) |
| { |
| if (!*repos_url) |
| SVN_ERR(svn_uri_get_file_url_from_dirent(repos_url, repos_path, pool)); |
| |
| SVN_ERR(svn_path_resolve_repos_relative_url(path, *path, |
| *repos_url, pool)); |
| *path = svn_uri_canonicalize(*path, pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* |
| * Get the, possibly cached, svn_authz_t for this request. |
| */ |
| static svn_authz_t * |
| get_access_conf(request_rec *r, authz_svn_config_rec *conf, |
| apr_pool_t *scratch_pool) |
| { |
| const char *cache_key = NULL; |
| const char *access_file; |
| const char *groups_file; |
| const char *repos_path; |
| const char *repos_url = NULL; |
| void *user_data = NULL; |
| svn_authz_t *access_conf = NULL; |
| svn_error_t *svn_err = SVN_NO_ERROR; |
| dav_error *dav_err; |
| |
| dav_err = dav_svn_get_repos_path2(r, conf->base_path, &repos_path, scratch_pool); |
| if (dav_err) |
| { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "%s", dav_err->desc); |
| return NULL; |
| } |
| |
| if (conf->repo_relative_access_file) |
| { |
| access_file = conf->repo_relative_access_file; |
| if (!svn_path_is_repos_relative_url(access_file) && |
| !svn_path_is_url(access_file)) |
| { |
| access_file = svn_dirent_join_many(scratch_pool, repos_path, "conf", |
| conf->repo_relative_access_file, |
| SVN_VA_NULL); |
| } |
| } |
| else |
| { |
| access_file = conf->access_file; |
| } |
| groups_file = conf->groups_file; |
| |
| svn_err = resolve_repos_relative_url(&access_file, &repos_url, repos_path, |
| scratch_pool); |
| if (svn_err) |
| { |
| log_svn_error(APLOG_MARK, r, |
| conf->repo_relative_access_file ? |
| "Failed to load the AuthzSVNReposRelativeAccessFile:" : |
| "Failed to load the AuthzSVNAccessFile:", |
| svn_err, scratch_pool); |
| return NULL; |
| } |
| |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, |
| "Path to authz file is %s", access_file); |
| |
| if (groups_file) |
| { |
| svn_err = resolve_repos_relative_url(&groups_file, &repos_url, repos_path, |
| scratch_pool); |
| if (svn_err) |
| { |
| log_svn_error(APLOG_MARK, r, |
| "Failed to load the AuthzSVNGroupsFile:", |
| svn_err, scratch_pool); |
| return NULL; |
| } |
| |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, |
| "Path to groups file is %s", groups_file); |
| } |
| |
| cache_key = apr_pstrcat(scratch_pool, "mod_authz_svn:", |
| access_file, groups_file, SVN_VA_NULL); |
| apr_pool_userdata_get(&user_data, cache_key, r->connection->pool); |
| access_conf = user_data; |
| if (access_conf == NULL) |
| { |
| |
| svn_err = svn_repos_authz_read2(&access_conf, access_file, |
| groups_file, TRUE, |
| r->connection->pool); |
| |
| if (svn_err) |
| { |
| log_svn_error(APLOG_MARK, r, |
| "Failed to load the mod_authz_svn config:", |
| svn_err, scratch_pool); |
| access_conf = NULL; |
| } |
| else |
| { |
| /* Cache the open repos for the next request on this connection */ |
| apr_pool_userdata_set(access_conf, cache_key, |
| NULL, r->connection->pool); |
| } |
| } |
| return access_conf; |
| } |
| |
| /* Convert TEXT to upper case if TO_UPPERCASE is TRUE, else |
| converts it to lower case. */ |
| static void |
| convert_case(char *text, svn_boolean_t to_uppercase) |
| { |
| char *c = text; |
| while (*c) |
| { |
| *c = (char)(to_uppercase ? apr_toupper(*c) : apr_tolower(*c)); |
| ++c; |
| } |
| } |
| |
| /* Return the username to authorize, with case-conversion performed if |
| CONF->force_username_case is set. */ |
| static char * |
| get_username_to_authorize(request_rec *r, authz_svn_config_rec *conf, |
| apr_pool_t *pool) |
| { |
| char *username_to_authorize = r->user; |
| if (username_to_authorize && conf->force_username_case) |
| { |
| username_to_authorize = apr_pstrdup(pool, r->user); |
| convert_case(username_to_authorize, |
| strcasecmp(conf->force_username_case, "upper") == 0); |
| } |
| return username_to_authorize; |
| } |
| |
| /* Check if the current request R is allowed. Upon exit *REPOS_PATH_REF |
| * will contain the path and repository name that an operation was requested |
| * on in the form 'name:path'. *DEST_REPOS_PATH_REF will contain the |
| * destination path if the requested operation was a MOVE or a COPY. |
| * Returns OK when access is allowed, DECLINED when it isn't, or an HTTP_ |
| * error code when an error occurred. |
| */ |
| static int |
| req_check_access(request_rec *r, |
| authz_svn_config_rec *conf, |
| const char **repos_path_ref, |
| const char **dest_repos_path_ref) |
| { |
| const char *dest_uri; |
| apr_uri_t parsed_dest_uri; |
| const char *cleaned_uri; |
| int trailing_slash; |
| const char *repos_name; |
| const char *dest_repos_name; |
| const char *relative_path; |
| const char *repos_path; |
| const char *dest_repos_path = NULL; |
| dav_error *dav_err; |
| svn_repos_authz_access_t authz_svn_type = svn_authz_none; |
| svn_boolean_t authz_access_granted = FALSE; |
| svn_authz_t *access_conf = NULL; |
| svn_error_t *svn_err; |
| const char *username_to_authorize = get_username_to_authorize(r, conf, |
| r->pool); |
| |
| switch (r->method_number) |
| { |
| /* All methods requiring read access to all subtrees of r->uri */ |
| case M_COPY: |
| authz_svn_type |= svn_authz_recursive; |
| |
| /* All methods requiring read access to r->uri */ |
| case M_OPTIONS: |
| case M_GET: |
| case M_PROPFIND: |
| case M_REPORT: |
| authz_svn_type |= svn_authz_read; |
| break; |
| |
| /* All methods requiring write access to all subtrees of r->uri */ |
| case M_MOVE: |
| case M_DELETE: |
| authz_svn_type |= svn_authz_recursive; |
| |
| /* All methods requiring write access to r->uri */ |
| case M_MKCOL: |
| case M_PUT: |
| case M_PROPPATCH: |
| case M_CHECKOUT: |
| case M_MERGE: |
| case M_MKACTIVITY: |
| case M_LOCK: |
| case M_UNLOCK: |
| authz_svn_type |= svn_authz_write; |
| break; |
| |
| default: |
| /* Require most strict access for unknown methods */ |
| authz_svn_type |= svn_authz_write | svn_authz_recursive; |
| break; |
| } |
| |
| if (strcmp(svn_urlpath__canonicalize(r->uri, r->pool), conf->base_path) == 0) |
| { |
| /* Do no access control when conf->base_path(as configured in <Location>) |
| * and given uri are same. The reason for such relaxation of access |
| * control is "This module is meant to control access inside the |
| * repository path, in this case inside PATH is empty and hence |
| * dav_svn_split_uri fails saying no repository name present". |
| * One may ask it will allow access to '/' inside the repository if |
| * repository is served via SVNPath instead of SVNParentPath. |
| * It does not, The other methods(PROPFIND, MKACTIVITY) for |
| * accomplishing the operation takes care of making a request to |
| * proper URL */ |
| return OK; |
| } |
| |
| dav_err = dav_svn_split_uri(r, |
| r->uri, |
| conf->base_path, |
| &cleaned_uri, |
| &trailing_slash, |
| &repos_name, |
| &relative_path, |
| &repos_path); |
| if (dav_err) |
| { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, |
| "%s [%d, #%d]", |
| dav_err->desc, dav_err->status, dav_err->error_id); |
| /* Ensure that we never allow access by dav_err->status */ |
| return (dav_err->status != OK && dav_err->status != DECLINED) ? |
| dav_err->status : HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| /* Ignore the URI passed to MERGE, like mod_dav_svn does. |
| * See issue #1821. |
| * XXX: When we start accepting a broader range of DeltaV MERGE |
| * XXX: requests, this should be revisited. |
| */ |
| if (r->method_number == M_MERGE) |
| repos_path = NULL; |
| |
| if (repos_path) |
| repos_path = svn_fspath__canonicalize(repos_path, r->pool); |
| |
| *repos_path_ref = apr_pstrcat(r->pool, repos_name, ":", repos_path, |
| SVN_VA_NULL); |
| |
| if (r->method_number == M_MOVE || r->method_number == M_COPY) |
| { |
| dest_uri = apr_table_get(r->headers_in, "Destination"); |
| |
| /* Decline MOVE or COPY when there is no Destination uri, this will |
| * cause failure. |
| */ |
| if (!dest_uri) |
| return DECLINED; |
| |
| apr_uri_parse(r->pool, dest_uri, &parsed_dest_uri); |
| |
| ap_unescape_url(parsed_dest_uri.path); |
| dest_uri = parsed_dest_uri.path; |
| if (strncmp(dest_uri, conf->base_path, strlen(conf->base_path))) |
| { |
| /* If it is not the same location, then we don't allow it. |
| * XXX: Instead we could compare repository uuids, but that |
| * XXX: seems a bit over the top. |
| */ |
| return HTTP_BAD_REQUEST; |
| } |
| |
| dav_err = dav_svn_split_uri(r, |
| dest_uri, |
| conf->base_path, |
| &cleaned_uri, |
| &trailing_slash, |
| &dest_repos_name, |
| &relative_path, |
| &dest_repos_path); |
| |
| if (dav_err) |
| { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, |
| "%s [%d, #%d]", |
| dav_err->desc, dav_err->status, dav_err->error_id); |
| /* Ensure that we never allow access by dav_err->status */ |
| return (dav_err->status != OK && dav_err->status != DECLINED) ? |
| dav_err->status : HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| if (dest_repos_path) |
| dest_repos_path = svn_fspath__canonicalize(dest_repos_path, r->pool); |
| |
| *dest_repos_path_ref = apr_pstrcat(r->pool, dest_repos_name, ":", |
| dest_repos_path, SVN_VA_NULL); |
| } |
| |
| /* Retrieve/cache authorization file */ |
| access_conf = get_access_conf(r,conf, r->pool); |
| if (access_conf == NULL) |
| return DECLINED; |
| |
| /* Perform authz access control. |
| * |
| * First test the special case where repos_path == NULL, and skip |
| * calling the authz routines in that case. This is an oddity of |
| * the DAV RA method: some requests have no repos_path, but apache |
| * still triggers an authz lookup for the URI. |
| * |
| * However, if repos_path == NULL and the request requires write |
| * access, then perform a global authz lookup. The request is |
| * denied if the user commiting isn't granted any access anywhere |
| * in the repository. This is to avoid operations that involve no |
| * paths (commiting an empty revision, leaving a dangling |
| * transaction in the FS) being granted by default, letting |
| * unauthenticated users write some changes to the repository. |
| * This was issue #2388. |
| * |
| * XXX: For now, requesting access to the entire repository always |
| * XXX: succeeds, until we come up with a good way of figuring |
| * XXX: this out. |
| */ |
| if (repos_path |
| || (!repos_path && (authz_svn_type & svn_authz_write))) |
| { |
| svn_err = svn_repos_authz_check_access(access_conf, repos_name, |
| repos_path, |
| username_to_authorize, |
| authz_svn_type, |
| &authz_access_granted, |
| r->pool); |
| if (svn_err) |
| { |
| log_svn_error(APLOG_MARK, r, |
| "Failed to perform access control:", |
| svn_err, r->pool); |
| |
| return DECLINED; |
| } |
| if (!authz_access_granted) |
| return DECLINED; |
| } |
| |
| /* XXX: MKCOL, MOVE, DELETE |
| * XXX: Require write access to the parent dir of repos_path. |
| */ |
| |
| /* XXX: PUT |
| * XXX: If the path doesn't exist, require write access to the |
| * XXX: parent dir of repos_path. |
| */ |
| |
| /* Only MOVE and COPY have a second uri we have to check access to. */ |
| if (r->method_number != M_MOVE && r->method_number != M_COPY) |
| return OK; |
| |
| /* Check access on the destination repos_path. Again, skip this if |
| repos_path == NULL (see above for explanations) */ |
| if (repos_path) |
| { |
| svn_err = svn_repos_authz_check_access(access_conf, |
| dest_repos_name, |
| dest_repos_path, |
| username_to_authorize, |
| svn_authz_write |
| |svn_authz_recursive, |
| &authz_access_granted, |
| r->pool); |
| if (svn_err) |
| { |
| log_svn_error(APLOG_MARK, r, |
| "Failed to perform access control:", |
| svn_err, r->pool); |
| |
| return DECLINED; |
| } |
| if (!authz_access_granted) |
| return DECLINED; |
| } |
| |
| /* XXX: MOVE and COPY, if the path doesn't exist yet, also |
| * XXX: require write access to the parent dir of dest_repos_path. |
| */ |
| |
| return OK; |
| } |
| |
| /* |
| * Implementation of subreq_bypass with scratch_pool parameter. |
| */ |
| static int |
| subreq_bypass2(request_rec *r, |
| const char *repos_path, |
| const char *repos_name, |
| apr_pool_t *scratch_pool) |
| { |
| svn_error_t *svn_err = NULL; |
| svn_authz_t *access_conf = NULL; |
| authz_svn_config_rec *conf = NULL; |
| svn_boolean_t authz_access_granted = FALSE; |
| const char *username_to_authorize; |
| |
| conf = ap_get_module_config(r->per_dir_config, |
| &authz_svn_module); |
| username_to_authorize = get_username_to_authorize(r, conf, scratch_pool); |
| |
| /* If configured properly, this should never be true, but just in case. */ |
| if (!conf->anonymous |
| || (! (conf->access_file || conf->repo_relative_access_file))) |
| { |
| log_access_verdict(APLOG_MARK, r, 0, TRUE, repos_path, NULL); |
| return HTTP_FORBIDDEN; |
| } |
| |
| /* Retrieve authorization file */ |
| access_conf = get_access_conf(r, conf, scratch_pool); |
| if (access_conf == NULL) |
| return HTTP_FORBIDDEN; |
| |
| /* Perform authz access control. |
| * See similarly labeled comment in req_check_access. |
| */ |
| if (repos_path) |
| { |
| svn_err = svn_repos_authz_check_access(access_conf, repos_name, |
| repos_path, |
| username_to_authorize, |
| svn_authz_none|svn_authz_read, |
| &authz_access_granted, |
| scratch_pool); |
| if (svn_err) |
| { |
| log_svn_error(APLOG_MARK, r, |
| "Failed to perform access control:", |
| svn_err, scratch_pool); |
| return HTTP_FORBIDDEN; |
| } |
| if (!authz_access_granted) |
| { |
| log_access_verdict(APLOG_MARK, r, 0, TRUE, repos_path, NULL); |
| return HTTP_FORBIDDEN; |
| } |
| } |
| |
| log_access_verdict(APLOG_MARK, r, 1, TRUE, repos_path, NULL); |
| |
| return OK; |
| } |
| |
| /* |
| * This function is used as a provider to allow mod_dav_svn to bypass the |
| * generation of an apache request when checking GET access from |
| * "mod_dav_svn/authz.c" . |
| */ |
| static int |
| subreq_bypass(request_rec *r, |
| const char *repos_path, |
| const char *repos_name) |
| { |
| int status; |
| apr_pool_t *scratch_pool; |
| |
| scratch_pool = svn_pool_create(r->pool); |
| status = subreq_bypass2(r, repos_path, repos_name, scratch_pool); |
| svn_pool_destroy(scratch_pool); |
| |
| return status; |
| } |
| |
| /* |
| * Hooks |
| */ |
| |
| static int |
| access_checker(request_rec *r) |
| { |
| authz_svn_config_rec *conf = ap_get_module_config(r->per_dir_config, |
| &authz_svn_module); |
| const char *repos_path = NULL; |
| const char *dest_repos_path = NULL; |
| int status, authn_required; |
| |
| #if USE_FORCE_AUTHN |
| /* Use the force_authn() hook available in 2.4.x to work securely |
| * given that ap_some_auth_required() is no longer functional for our |
| * purposes in 2.4.x. |
| */ |
| int authn_configured; |
| |
| /* We are not configured to run */ |
| if (!conf->anonymous || apr_table_get(r->notes, IN_SOME_AUTHN_NOTE) |
| || (! (conf->access_file || conf->repo_relative_access_file))) |
| return DECLINED; |
| |
| /* Authentication is configured */ |
| authn_configured = ap_auth_type(r) != NULL; |
| if (authn_configured) |
| { |
| /* If the user is trying to authenticate, let him. It doesn't |
| * make much sense to grant anonymous access but deny authenticated |
| * users access, even though you can do that with '$anon' in the |
| * access file. |
| */ |
| if (apr_table_get(r->headers_in, |
| (PROXYREQ_PROXY == r->proxyreq) |
| ? "Proxy-Authorization" : "Authorization")) |
| { |
| /* Set the note to force authn regardless of what access_checker_ex |
| hook requires */ |
| apr_table_setn(r->notes, FORCE_AUTHN_NOTE, (const char*)1); |
| |
| /* provide the proper return so the access_checker hook doesn't |
| * prevent the code from continuing on to the other auth hooks */ |
| if (ap_satisfies(r) != SATISFY_ANY) |
| return OK; |
| else |
| return HTTP_FORBIDDEN; |
| } |
| } |
| |
| #else |
| /* Support for older versions of httpd that have a working |
| * ap_some_auth_required() */ |
| |
| /* We are not configured to run */ |
| if (!conf->anonymous |
| || (! (conf->access_file || conf->repo_relative_access_file))) |
| return DECLINED; |
| |
| authn_required = ap_some_auth_required(r); |
| if (authn_required) |
| { |
| /* It makes no sense to check if a location is both accessible |
| * anonymous and by an authenticated user (in the same request!). |
| */ |
| if (ap_satisfies(r) != SATISFY_ANY) |
| return DECLINED; |
| |
| /* If the user is trying to authenticate, let him. It doesn't |
| * make much sense to grant anonymous access but deny authenticated |
| * users access, even though you can do that with '$anon' in the |
| * access file. |
| */ |
| if (apr_table_get(r->headers_in, |
| (PROXYREQ_PROXY == r->proxyreq) |
| ? "Proxy-Authorization" : "Authorization")) |
| { |
| /* Given Satisfy Any is in effect, we have to forbid access |
| * to let the auth_checker hook have a go at it. |
| */ |
| return HTTP_FORBIDDEN; |
| } |
| } |
| #endif |
| |
| /* If anon access is allowed, return OK */ |
| status = req_check_access(r, conf, &repos_path, &dest_repos_path); |
| if (status == DECLINED) |
| { |
| if (!conf->authoritative) |
| return DECLINED; |
| |
| #if USE_FORCE_AUTHN |
| if (authn_configured) { |
| /* We have to check to see if authn is required because if so we must |
| * return UNAUTHORIZED (401) rather than FORBIDDEN (403) since returning |
| * the 403 leaks information about what paths may exist to |
| * unauthenticated users. We must set a note here in order |
| * to use ap_some_authn_rquired() without triggering an infinite |
| * loop since the call will trigger this function to be called again. */ |
| apr_table_setn(r->notes, IN_SOME_AUTHN_NOTE, (const char*)1); |
| authn_required = ap_some_authn_required(r); |
| apr_table_unset(r->notes, IN_SOME_AUTHN_NOTE); |
| if (authn_required) |
| { |
| ap_note_auth_failure(r); |
| return HTTP_UNAUTHORIZED; |
| } |
| } |
| #else |
| if (!authn_required) |
| #endif |
| log_access_verdict(APLOG_MARK, r, 0, FALSE, repos_path, dest_repos_path); |
| |
| return HTTP_FORBIDDEN; |
| } |
| |
| if (status != OK) |
| return status; |
| |
| log_access_verdict(APLOG_MARK, r, 1, FALSE, repos_path, dest_repos_path); |
| |
| return OK; |
| } |
| |
| static int |
| check_user_id(request_rec *r) |
| { |
| authz_svn_config_rec *conf = ap_get_module_config(r->per_dir_config, |
| &authz_svn_module); |
| const char *repos_path = NULL; |
| const char *dest_repos_path = NULL; |
| int status; |
| |
| /* We are not configured to run, or, an earlier module has already |
| * authenticated this request. */ |
| if (!conf->no_auth_when_anon_ok || r->user |
| || (! (conf->access_file || conf->repo_relative_access_file))) |
| return DECLINED; |
| |
| /* If anon access is allowed, return OK, preventing later modules |
| * from issuing an HTTP_UNAUTHORIZED. Also pass a note to our |
| * auth_checker hook that access has already been checked. */ |
| status = req_check_access(r, conf, &repos_path, &dest_repos_path); |
| if (status == OK) |
| { |
| apr_table_setn(r->notes, "authz_svn-anon-ok", (const char*)1); |
| log_access_verdict(APLOG_MARK, r, 1, FALSE, repos_path, dest_repos_path); |
| return OK; |
| } |
| |
| return status; |
| } |
| |
| static int |
| auth_checker(request_rec *r) |
| { |
| authz_svn_config_rec *conf = ap_get_module_config(r->per_dir_config, |
| &authz_svn_module); |
| const char *repos_path = NULL; |
| const char *dest_repos_path = NULL; |
| int status; |
| |
| /* We are not configured to run */ |
| if (! (conf->access_file || conf->repo_relative_access_file)) |
| return DECLINED; |
| |
| /* Previous hook (check_user_id) already did all the work, |
| * and, as a sanity check, r->user hasn't been set since then? */ |
| if (!r->user && apr_table_get(r->notes, "authz_svn-anon-ok")) |
| return OK; |
| |
| status = req_check_access(r, conf, &repos_path, &dest_repos_path); |
| if (status == DECLINED) |
| { |
| if (conf->authoritative) |
| { |
| log_access_verdict(APLOG_MARK, r, 0, FALSE, repos_path, dest_repos_path); |
| ap_note_auth_failure(r); |
| return HTTP_FORBIDDEN; |
| } |
| return DECLINED; |
| } |
| |
| if (status != OK) |
| return status; |
| |
| log_access_verdict(APLOG_MARK, r, 1, FALSE, repos_path, dest_repos_path); |
| |
| return OK; |
| } |
| |
| #if USE_FORCE_AUTHN |
| static int |
| force_authn(request_rec *r) |
| { |
| if (apr_table_get(r->notes, FORCE_AUTHN_NOTE)) |
| return OK; |
| |
| return DECLINED; |
| } |
| #endif |
| |
| /* |
| * Module flesh |
| */ |
| |
| /* Implements the #register_hooks method of Apache's #module vtable. */ |
| static void |
| register_hooks(apr_pool_t *p) |
| { |
| static const char * const mod_ssl[] = { "mod_ssl.c", NULL }; |
| |
| ap_hook_access_checker(access_checker, NULL, NULL, APR_HOOK_LAST); |
| /* Our check_user_id hook must be before any module which will return |
| * HTTP_UNAUTHORIZED (mod_auth_basic, etc.), but after mod_ssl, to |
| * give SSLOptions +FakeBasicAuth a chance to work. */ |
| ap_hook_check_user_id(check_user_id, mod_ssl, NULL, APR_HOOK_FIRST); |
| ap_hook_auth_checker(auth_checker, NULL, NULL, APR_HOOK_FIRST); |
| #if USE_FORCE_AUTHN |
| ap_hook_force_authn(force_authn, NULL, NULL, APR_HOOK_FIRST); |
| #endif |
| ap_register_provider(p, |
| AUTHZ_SVN__SUBREQ_BYPASS_PROV_GRP, |
| AUTHZ_SVN__SUBREQ_BYPASS_PROV_NAME, |
| AUTHZ_SVN__SUBREQ_BYPASS_PROV_VER, |
| (void*)subreq_bypass); |
| } |
| |
| module AP_MODULE_DECLARE_DATA authz_svn_module = |
| { |
| STANDARD20_MODULE_STUFF, |
| create_authz_svn_dir_config, /* dir config creater */ |
| NULL, /* dir merger --- default is to override */ |
| NULL, /* server config */ |
| NULL, /* merge server config */ |
| authz_svn_cmds, /* command apr_table_t */ |
| register_hooks /* register hooks */ |
| }; |