1 /*
2  * mod_authz_svn.c: an Apache mod_dav_svn sub-module to provide path
3  *                  based authorization for a Subversion repository.
4  *
5  * ====================================================================
6  *    Licensed to the Apache Software Foundation (ASF) under one
7  *    or more contributor license agreements.  See the NOTICE file
8  *    distributed with this work for additional information
9  *    regarding copyright ownership.  The ASF licenses this file
10  *    to you under the Apache License, Version 2.0 (the
11  *    "License"); you may not use this file except in compliance
12  *    with the License.  You may obtain a copy of the License at
13  *
14  *      http://www.apache.org/licenses/LICENSE-2.0
15  *
16  *    Unless required by applicable law or agreed to in writing,
17  *    software distributed under the License is distributed on an
18  *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
19  *    KIND, either express or implied.  See the License for the
20  *    specific language governing permissions and limitations
21  *    under the License.
22  * ====================================================================
23  */
24 
25 
26 
27 #include <httpd.h>
28 #include <http_config.h>
29 #include <http_core.h>
30 #include <http_request.h>
31 #include <http_protocol.h>
32 #include <http_log.h>
33 #include <http_config.h>
34 #include <ap_config.h>
35 #include <ap_provider.h>
36 #include <ap_mmn.h>
37 #include <apr_uri.h>
38 #include <apr_lib.h>
39 #include <mod_dav.h>
40 
41 #include "mod_dav_svn.h"
42 #include "mod_authz_svn.h"
43 #include "svn_path.h"
44 #include "svn_config.h"
45 #include "svn_string.h"
46 #include "svn_repos.h"
47 #include "svn_pools.h"
48 #include "svn_dirent_uri.h"
49 #include "private/svn_fspath.h"
50 
51 /* The apache headers define these and they conflict with our definitions. */
52 #ifdef PACKAGE_BUGREPORT
53 #undef PACKAGE_BUGREPORT
54 #endif
55 #ifdef PACKAGE_NAME
56 #undef PACKAGE_NAME
57 #endif
58 #ifdef PACKAGE_STRING
59 #undef PACKAGE_STRING
60 #endif
61 #ifdef PACKAGE_TARNAME
62 #undef PACKAGE_TARNAME
63 #endif
64 #ifdef PACKAGE_VERSION
65 #undef PACKAGE_VERSION
66 #endif
67 #include "svn_private_config.h"
68 
69 #ifdef APLOG_USE_MODULE
70 APLOG_USE_MODULE(authz_svn);
71 #else
72 /* This is part of the APLOG_USE_MODULE() macro in httpd-2.3 */
73 extern module AP_MODULE_DECLARE_DATA authz_svn_module;
74 #endif
75 
76 typedef struct authz_svn_config_rec {
77   int authoritative;
78   int anonymous;
79   int no_auth_when_anon_ok;
80   const char *base_path;
81   const char *access_file;
82   const char *repo_relative_access_file;
83   const char *groups_file;
84   const char *force_username_case;
85 } authz_svn_config_rec;
86 
87 /* version where ap_some_auth_required breaks */
88 #if AP_MODULE_MAGIC_AT_LEAST(20060110,0)
89 /* first version with force_authn hook and ap_some_authn_required()
90    which allows us to work without ap_some_auth_required() */
91 #  if AP_MODULE_MAGIC_AT_LEAST(20120211,47) || defined(SVN_USE_FORCE_AUTHN)
92 #    define USE_FORCE_AUTHN 1
93 #    define IN_SOME_AUTHN_NOTE "authz_svn-in-some-authn"
94 #    define FORCE_AUTHN_NOTE "authz_svn-force-authn"
95 #  else
96      /* ap_some_auth_required() is busted and no viable alternative exists */
97 #    ifndef SVN_ALLOW_BROKEN_HTTPD_AUTH
98 #      error This Apache httpd has broken auth (CVE-2015-3184)
99 #    else
100        /* user wants to build anyway */
101 #      define USE_FORCE_AUTHN 0
102 #    endif
103 #  endif
104 #else
105    /* old enough that ap_some_auth_required() still works */
106 #  define USE_FORCE_AUTHN 0
107 #endif
108 
109 /*
110  * Configuration
111  */
112 
113 /* Implements the #create_dir_config method of Apache's #module vtable. */
114 static void *
create_authz_svn_dir_config(apr_pool_t * p,char * d)115 create_authz_svn_dir_config(apr_pool_t *p, char *d)
116 {
117   authz_svn_config_rec *conf = apr_pcalloc(p, sizeof(*conf));
118   conf->base_path = d;
119 
120   if (d)
121     conf->base_path = svn_urlpath__canonicalize(d, p);
122 
123   /* By default keep the fortress secure */
124   conf->authoritative = 1;
125   conf->anonymous = 1;
126 
127   return conf;
128 }
129 
130 /* canonicalize ACCESS_FILE based on the type of argument.
131  * If SERVER_RELATIVE is true, ACCESS_FILE is a relative
132  * path then ACCESS_FILE is converted to an absolute
133  * path rooted at the server root.
134  * Returns NULL if path is not valid.*/
135 static const char *
canonicalize_access_file(const char * access_file,svn_boolean_t server_relative,apr_pool_t * pool)136 canonicalize_access_file(const char *access_file,
137                          svn_boolean_t server_relative,
138                          apr_pool_t *pool)
139 {
140   if (svn_path_is_url(access_file))
141     {
142       access_file = svn_uri_canonicalize(access_file, pool);
143     }
144   else if (!svn_path_is_repos_relative_url(access_file))
145     {
146       if (server_relative)
147         {
148           access_file = ap_server_root_relative(pool, access_file);
149           if (access_file == NULL)
150             return NULL;
151         }
152 
153       access_file = svn_dirent_internal_style(access_file, pool);
154     }
155 
156   /* We don't canonicalize repos relative urls since they get
157    * canonicalized before calling svn_repos_authz_read3() when they
158    * are resolved. */
159 
160   return access_file;
161 }
162 
163 static const char *
AuthzSVNAccessFile_cmd(cmd_parms * cmd,void * config,const char * arg1)164 AuthzSVNAccessFile_cmd(cmd_parms *cmd, void *config, const char *arg1)
165 {
166   authz_svn_config_rec *conf = config;
167 
168   if (conf->repo_relative_access_file != NULL)
169     return "AuthzSVNAccessFile and AuthzSVNReposRelativeAccessFile "
170            "directives are mutually exclusive.";
171 
172   conf->access_file = canonicalize_access_file(arg1, TRUE, cmd->pool);
173   if (!conf->access_file)
174     return apr_pstrcat(cmd->pool, "Invalid file path ", arg1, SVN_VA_NULL);
175 
176   return NULL;
177 }
178 
179 
180 static const char *
AuthzSVNReposRelativeAccessFile_cmd(cmd_parms * cmd,void * config,const char * arg1)181 AuthzSVNReposRelativeAccessFile_cmd(cmd_parms *cmd,
182                                     void *config,
183                                     const char *arg1)
184 {
185   authz_svn_config_rec *conf = config;
186 
187   if (conf->access_file != NULL)
188     return "AuthzSVNAccessFile and AuthzSVNReposRelativeAccessFile "
189            "directives are mutually exclusive.";
190 
191   conf->repo_relative_access_file = canonicalize_access_file(arg1, FALSE,
192                                                              cmd->pool);
193 
194   if (!conf->repo_relative_access_file)
195     return apr_pstrcat(cmd->pool, "Invalid file path ", arg1, SVN_VA_NULL);
196 
197   return NULL;
198 }
199 
200 static const char *
AuthzSVNGroupsFile_cmd(cmd_parms * cmd,void * config,const char * arg1)201 AuthzSVNGroupsFile_cmd(cmd_parms *cmd, void *config, const char *arg1)
202 {
203   authz_svn_config_rec *conf = config;
204 
205   conf->groups_file = canonicalize_access_file(arg1, TRUE, cmd->pool);
206 
207   if (!conf->groups_file)
208     return apr_pstrcat(cmd->pool, "Invalid file path ", arg1, SVN_VA_NULL);
209 
210   return NULL;
211 }
212 
213 /* Implements the #cmds member of Apache's #module vtable. */
214 static const command_rec authz_svn_cmds[] =
215 {
216   AP_INIT_FLAG("AuthzSVNAuthoritative", ap_set_flag_slot,
217                (void *)APR_OFFSETOF(authz_svn_config_rec, authoritative),
218                OR_AUTHCFG,
219                "Set to 'Off' to allow access control to be passed along to "
220                "lower modules. (default is On.)"),
221   AP_INIT_TAKE1("AuthzSVNAccessFile", AuthzSVNAccessFile_cmd,
222                 NULL,
223                 OR_AUTHCFG,
224                 "Path to text file containing permissions of repository "
225                 "paths.  Path may be an repository relative URL (^/) or "
226                 "absolute file:// URL to a text file in a Subversion "
227                 "repository."),
228   AP_INIT_TAKE1("AuthzSVNReposRelativeAccessFile",
229                 AuthzSVNReposRelativeAccessFile_cmd,
230                 NULL,
231                 OR_AUTHCFG,
232                 "Path (relative to repository 'conf' directory) to text "
233                 "file containing permissions of repository paths. Path may "
234                 "be an repository relative URL (^/) or absolute file:// URL "
235                 "to a text file in a Subversion repository."),
236   AP_INIT_TAKE1("AuthzSVNGroupsFile",
237                 AuthzSVNGroupsFile_cmd,
238                 NULL,
239                 OR_AUTHCFG,
240                 "Path to text file containing group definitions for all "
241                 "repositories.  Path may be an repository relative URL (^/) "
242                 "or absolute file:// URL to a text file in a Subversion "
243                 "repository."),
244   AP_INIT_FLAG("AuthzSVNAnonymous", ap_set_flag_slot,
245                (void *)APR_OFFSETOF(authz_svn_config_rec, anonymous),
246                OR_AUTHCFG,
247                "Set to 'Off' to disable two special-case behaviours of "
248                "this module: (1) interaction with the 'Satisfy Any' "
249                "directive, and (2) enforcement of the authorization "
250                "policy even when no 'Require' directives are present. "
251                "(default is On.)"),
252   AP_INIT_FLAG("AuthzSVNNoAuthWhenAnonymousAllowed", ap_set_flag_slot,
253                (void *)APR_OFFSETOF(authz_svn_config_rec,
254                                     no_auth_when_anon_ok),
255                OR_AUTHCFG,
256                "Set to 'On' to suppress authentication and authorization "
257                "for requests which anonymous users are allowed to perform. "
258                "(default is Off.)"),
259   AP_INIT_TAKE1("AuthzForceUsernameCase", ap_set_string_slot,
260                 (void *)APR_OFFSETOF(authz_svn_config_rec,
261                                      force_username_case),
262                 OR_AUTHCFG,
263                 "Set to 'Upper' or 'Lower' to convert the username before "
264                 "checking for authorization."),
265   { NULL }
266 };
267 
268 
269 /* The macros LOG_ARGS_SIGNATURE and LOG_ARGS_CASCADE are expanded as formal
270  * and actual parameters to log_access_verdict with respect to HTTPD version.
271  */
272 #if AP_MODULE_MAGIC_AT_LEAST(20100606,0)
273 #define LOG_ARGS_SIGNATURE const char *file, int line, int module_index
274 #define LOG_ARGS_CASCADE file, line, module_index
275 #else
276 #define LOG_ARGS_SIGNATURE const char *file, int line
277 #define LOG_ARGS_CASCADE file, line
278 #endif
279 
280 /* Log a message indicating the access control decision made about a
281  * request.  The macro LOG_ARGS_SIGNATURE expands to FILE, LINE and
282  * MODULE_INDEX in HTTPD 2.3 as APLOG_MARK macro has been changed for
283  * per-module loglevel configuration.  It expands to FILE and LINE
284  * in older server versions.  ALLOWED is boolean.
285  * REPOS_PATH and DEST_REPOS_PATH are information
286  * about the request.  DEST_REPOS_PATH may be NULL.
287  * Non-zero IS_SUBREQ_BYPASS means that this authorization check was
288  * implicitly requested using 'subrequest bypass' callback from
289  * mod_dav_svn.
290  */
291 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)292 log_access_verdict(LOG_ARGS_SIGNATURE,
293                    const request_rec *r, int allowed, int is_subreq_bypass,
294                    const char *repos_path, const char *dest_repos_path)
295 {
296   int level = allowed ? APLOG_INFO : APLOG_ERR;
297   const char *verdict = allowed ? "granted" : "denied";
298 
299   /* Use less important log level for implicit sub-request authorization
300      checks. */
301   if (is_subreq_bypass)
302     level = APLOG_INFO;
303   else if (r->main && r->method_number == M_GET)
304     level = APLOG_INFO;
305 
306   if (r->user)
307     {
308       if (dest_repos_path)
309         ap_log_rerror(LOG_ARGS_CASCADE, level, 0, r,
310                       "Access %s: '%s' %s %s %s", verdict, r->user,
311                       r->method, repos_path, dest_repos_path);
312       else
313         ap_log_rerror(LOG_ARGS_CASCADE, level, 0, r,
314                       "Access %s: '%s' %s %s", verdict, r->user,
315                       r->method, repos_path);
316     }
317   else
318     {
319       if (dest_repos_path)
320         ap_log_rerror(LOG_ARGS_CASCADE, level, 0, r,
321                       "Access %s: - %s %s %s", verdict,
322                       r->method, repos_path, dest_repos_path);
323       else
324         ap_log_rerror(LOG_ARGS_CASCADE, level, 0, r,
325                       "Access %s: - %s %s", verdict,
326                       r->method, repos_path);
327     }
328 }
329 
330 /* Log a message at LOG_LEVEL indiciating the ERR encountered during
331  * the request R.
332  * LOG_ARGS_SIGNATURE expands as in log_access_verdict() above.
333  * PREFIX is inserted at the start of the message.  The rest of the
334  * message is generated by combining the message for each error in the
335  * chain of ERR, excluding for trace errors.  ERR will be cleared
336  * when finished. */
337 static void
log_svn_message(LOG_ARGS_SIGNATURE,int log_level,request_rec * r,const char * prefix,svn_error_t * err,apr_pool_t * scratch_pool)338 log_svn_message(LOG_ARGS_SIGNATURE, int log_level,
339                 request_rec *r, const char *prefix,
340                 svn_error_t *err, apr_pool_t *scratch_pool)
341 {
342   svn_error_t *err_pos = svn_error_purge_tracing(err);
343   svn_stringbuf_t *buff = svn_stringbuf_create(prefix, scratch_pool);
344 
345   /* Build the error chain into a space separated stringbuf. */
346   while (err_pos)
347     {
348       svn_stringbuf_appendbyte(buff, ' ');
349       if (err_pos->message)
350         {
351           svn_stringbuf_appendcstr(buff, err_pos->message);
352         }
353       else
354         {
355           char strerr[256];
356 
357           svn_stringbuf_appendcstr(buff, svn_strerror(err->apr_err, strerr,
358                                                        sizeof(strerr)));
359         }
360 
361       err_pos = err_pos->child;
362     }
363 
364   ap_log_rerror(LOG_ARGS_CASCADE, log_level,
365                 /* If it is an error code that APR can make sense of, then
366                    show it, otherwise, pass zero to avoid putting "APR does
367                    not understand this error code" in the error log. */
368                 ((err->apr_err >= APR_OS_START_USERERR &&
369                   err->apr_err < APR_OS_START_CANONERR) ?
370                  0 : err->apr_err),
371                 r, "%s", buff->data);
372 
373   svn_error_clear(err);
374 }
375 
376 /* Log the error error ERR encountered during the request R.
377  * LOG_ARGS_SIGNATURE expands as in log_access_verdict() above.
378  * PREFIX is inserted at the start of the message.  The rest of the
379  * message is generated by combining the message for each error in the
380  * chain of ERR, excluding for trace errors.  ERR will be cleared
381  * when finished. */
382 static APR_INLINE void
log_svn_error(LOG_ARGS_SIGNATURE,request_rec * r,const char * prefix,svn_error_t * err,apr_pool_t * scratch_pool)383 log_svn_error(LOG_ARGS_SIGNATURE,
384               request_rec *r, const char *prefix,
385               svn_error_t *err, apr_pool_t *scratch_pool)
386 {
387   log_svn_message(LOG_ARGS_CASCADE, APLOG_ERR,
388                   r, prefix, err, scratch_pool);
389 }
390 
391 /* Baton for log_authz_warning. */
392 typedef struct authz_warning_baton_t
393 {
394   request_rec *r;
395   const char *prefix;
396 } authz_warning_baton_t;
397 
398 /* Handle an authz parser warning. ERR will *not* be cleared.*/
399 static APR_INLINE void
log_authz_warning(void * baton,const svn_error_t * err,apr_pool_t * scratch_pool)400 log_authz_warning(void *baton,
401                   const svn_error_t *err,
402                   apr_pool_t *scratch_pool)
403 {
404   const authz_warning_baton_t *const warning_baton = baton;
405   log_svn_message(APLOG_MARK, APLOG_WARNING,
406                   warning_baton->r, warning_baton->prefix,
407                   svn_error_dup(err), scratch_pool);
408 }
409 
410 /* Resolve *PATH into an absolute canonical URL iff *PATH is a repos-relative
411  * URL.  If *REPOS_URL is NULL convert REPOS_PATH into a file URL stored
412  * in *REPOS_URL, if *REPOS_URL is not null REPOS_PATH is ignored.  The
413  * resulting *REPOS_URL will be used as the root of the repos-relative URL.
414  * The result will be stored in *PATH. */
415 static svn_error_t *
resolve_repos_relative_url(const char ** path,const char ** repos_url,const char * repos_path,apr_pool_t * pool)416 resolve_repos_relative_url(const char **path, const char **repos_url,
417                            const char *repos_path, apr_pool_t *pool)
418 {
419   if (svn_path_is_repos_relative_url(*path))
420     {
421       if (!*repos_url)
422         SVN_ERR(svn_uri_get_file_url_from_dirent(repos_url, repos_path, pool));
423 
424       SVN_ERR(svn_path_resolve_repos_relative_url(path, *path,
425                                                   *repos_url, pool));
426       *path = svn_uri_canonicalize(*path, pool);
427     }
428 
429   return SVN_NO_ERROR;
430 }
431 
432 /*
433  * Get the, possibly cached, svn_authz_t for this request.
434  */
435 static svn_authz_t *
get_access_conf(request_rec * r,authz_svn_config_rec * conf,apr_pool_t * scratch_pool)436 get_access_conf(request_rec *r, authz_svn_config_rec *conf,
437                 apr_pool_t *scratch_pool)
438 {
439   const char *cache_key = NULL;
440   const char *access_file;
441   const char *groups_file;
442   const char *repos_path;
443   const char *repos_url = NULL;
444   void *user_data = NULL;
445   svn_authz_t *access_conf = NULL;
446   svn_error_t *svn_err = SVN_NO_ERROR;
447   dav_error *dav_err;
448 
449   dav_err = dav_svn_get_repos_path2(r, conf->base_path, &repos_path, scratch_pool);
450   if (dav_err)
451     {
452       ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "%s", dav_err->desc);
453       return NULL;
454     }
455 
456   if (conf->repo_relative_access_file)
457     {
458       access_file = conf->repo_relative_access_file;
459       if (!svn_path_is_repos_relative_url(access_file) &&
460           !svn_path_is_url(access_file))
461         {
462           access_file = svn_dirent_join_many(scratch_pool, repos_path, "conf",
463                                              conf->repo_relative_access_file,
464                                              SVN_VA_NULL);
465         }
466     }
467   else
468     {
469       access_file = conf->access_file;
470     }
471   groups_file = conf->groups_file;
472 
473   svn_err = resolve_repos_relative_url(&access_file, &repos_url, repos_path,
474                                        scratch_pool);
475   if (svn_err)
476     {
477       log_svn_error(APLOG_MARK, r,
478                     conf->repo_relative_access_file ?
479                     "Failed to load the AuthzSVNReposRelativeAccessFile:" :
480                     "Failed to load the AuthzSVNAccessFile:",
481                     svn_err, scratch_pool);
482       return NULL;
483     }
484 
485   ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
486                 "Path to authz file is %s", access_file);
487 
488   if (groups_file)
489     {
490       svn_err = resolve_repos_relative_url(&groups_file, &repos_url, repos_path,
491                                            scratch_pool);
492       if (svn_err)
493         {
494           log_svn_error(APLOG_MARK, r,
495                         "Failed to load the AuthzSVNGroupsFile:",
496                         svn_err, scratch_pool);
497           return NULL;
498         }
499 
500       ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
501                     "Path to groups file is %s", groups_file);
502     }
503 
504   cache_key = apr_pstrcat(scratch_pool, "mod_authz_svn:",
505                           access_file, groups_file, SVN_VA_NULL);
506   apr_pool_userdata_get(&user_data, cache_key, r->connection->pool);
507   access_conf = user_data;
508   if (access_conf == NULL)
509     {
510       authz_warning_baton_t warning_baton;
511       warning_baton.r = r;
512       warning_baton.prefix = "mod_authz_svn: warning:";
513 
514       svn_err = svn_repos_authz_read4(&access_conf, access_file,
515                                       groups_file, TRUE, NULL,
516                                       log_authz_warning, &warning_baton,
517                                       r->connection->pool,
518                                       scratch_pool);
519 
520       if (svn_err)
521         {
522           log_svn_error(APLOG_MARK, r,
523                         "Failed to load the mod_authz_svn config:",
524                         svn_err, scratch_pool);
525           access_conf = NULL;
526         }
527       else
528         {
529           /* Cache the open repos for the next request on this connection */
530           apr_pool_userdata_set(access_conf, cache_key,
531                                 NULL, r->connection->pool);
532         }
533     }
534   return access_conf;
535 }
536 
537 /* Convert TEXT to upper case if TO_UPPERCASE is TRUE, else
538    converts it to lower case. */
539 static void
convert_case(char * text,svn_boolean_t to_uppercase)540 convert_case(char *text, svn_boolean_t to_uppercase)
541 {
542   char *c = text;
543   while (*c)
544     {
545       *c = (char)(to_uppercase ? apr_toupper(*c) : apr_tolower(*c));
546       ++c;
547     }
548 }
549 
550 /* Return the username to authorize, with case-conversion performed if
551    CONF->force_username_case is set. */
552 static char *
get_username_to_authorize(request_rec * r,authz_svn_config_rec * conf,apr_pool_t * pool)553 get_username_to_authorize(request_rec *r, authz_svn_config_rec *conf,
554                           apr_pool_t *pool)
555 {
556   char *username_to_authorize = r->user;
557   if (username_to_authorize && conf->force_username_case)
558     {
559       username_to_authorize = apr_pstrdup(pool, r->user);
560       convert_case(username_to_authorize,
561                    strcasecmp(conf->force_username_case, "upper") == 0);
562     }
563   return username_to_authorize;
564 }
565 
566 /* Check if the current request R is allowed.  Upon exit *REPOS_PATH_REF
567  * will contain the path and repository name that an operation was requested
568  * on in the form 'name:path'.  *DEST_REPOS_PATH_REF will contain the
569  * destination path if the requested operation was a MOVE or a COPY.
570  * Returns OK when access is allowed, DECLINED when it isn't, or an HTTP_
571  * error code when an error occurred.
572  */
573 static int
req_check_access(request_rec * r,authz_svn_config_rec * conf,const char ** repos_path_ref,const char ** dest_repos_path_ref)574 req_check_access(request_rec *r,
575                  authz_svn_config_rec *conf,
576                  const char **repos_path_ref,
577                  const char **dest_repos_path_ref)
578 {
579   const char *dest_uri;
580   apr_uri_t parsed_dest_uri;
581   const char *cleaned_uri;
582   int trailing_slash;
583   const char *repos_name;
584   const char *dest_repos_name;
585   const char *relative_path;
586   const char *repos_path;
587   const char *dest_repos_path = NULL;
588   dav_error *dav_err;
589   svn_repos_authz_access_t authz_svn_type = svn_authz_none;
590   svn_boolean_t authz_access_granted = FALSE;
591   svn_authz_t *access_conf = NULL;
592   svn_error_t *svn_err;
593   const char *username_to_authorize = get_username_to_authorize(r, conf,
594                                                                 r->pool);
595 
596   switch (r->method_number)
597     {
598       /* All methods requiring read access to all subtrees of r->uri */
599       case M_COPY:
600         authz_svn_type |= svn_authz_recursive;
601 
602       /* All methods requiring read access to r->uri */
603       case M_OPTIONS:
604       case M_GET:
605       case M_PROPFIND:
606       case M_REPORT:
607         authz_svn_type |= svn_authz_read;
608         break;
609 
610       /* All methods requiring write access to all subtrees of r->uri */
611       case M_MOVE:
612       case M_DELETE:
613         authz_svn_type |= svn_authz_recursive;
614 
615       /* All methods requiring write access to r->uri */
616       case M_MKCOL:
617       case M_PUT:
618       case M_PROPPATCH:
619       case M_CHECKOUT:
620       case M_MERGE:
621       case M_MKACTIVITY:
622       case M_LOCK:
623       case M_UNLOCK:
624         authz_svn_type |= svn_authz_write;
625         break;
626 
627       default:
628         /* Require most strict access for unknown methods */
629         authz_svn_type |= svn_authz_write | svn_authz_recursive;
630         break;
631     }
632 
633   if (strcmp(svn_urlpath__canonicalize(r->uri, r->pool), conf->base_path) == 0)
634     {
635       /* Do no access control when conf->base_path(as configured in <Location>)
636        * and given uri are same. The reason for such relaxation of access
637        * control is "This module is meant to control access inside the
638        * repository path, in this case inside PATH is empty and hence
639        * dav_svn_split_uri fails saying no repository name present".
640        * One may ask it will allow access to '/' inside the repository if
641        * repository is served via SVNPath instead of SVNParentPath.
642        * It does not, The other methods(PROPFIND, MKACTIVITY) for
643        * accomplishing the operation takes care of making a request to
644        * proper URL */
645       return OK;
646     }
647 
648   dav_err = dav_svn_split_uri(r,
649                               r->uri,
650                               conf->base_path,
651                               &cleaned_uri,
652                               &trailing_slash,
653                               &repos_name,
654                               &relative_path,
655                               &repos_path);
656   if (dav_err)
657     {
658       ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
659                     "%s  [%d, #%d]",
660                     dav_err->desc, dav_err->status, dav_err->error_id);
661       /* Ensure that we never allow access by dav_err->status */
662       return (dav_err->status != OK && dav_err->status != DECLINED) ?
663               dav_err->status : HTTP_INTERNAL_SERVER_ERROR;
664     }
665 
666   /* Ignore the URI passed to MERGE, like mod_dav_svn does.
667    * See issue #1821.
668    * XXX: When we start accepting a broader range of DeltaV MERGE
669    * XXX: requests, this should be revisited.
670    */
671   if (r->method_number == M_MERGE)
672     repos_path = NULL;
673 
674   if (repos_path)
675     repos_path = svn_fspath__canonicalize(repos_path, r->pool);
676 
677   *repos_path_ref = apr_pstrcat(r->pool, repos_name, ":", repos_path,
678                                 SVN_VA_NULL);
679 
680   if (r->method_number == M_MOVE || r->method_number == M_COPY)
681     {
682       apr_status_t status;
683 
684       dest_uri = apr_table_get(r->headers_in, "Destination");
685 
686       /* Decline MOVE or COPY when there is no Destination uri, this will
687        * cause failure.
688        */
689       if (!dest_uri)
690         return DECLINED;
691 
692       status = apr_uri_parse(r->pool, dest_uri, &parsed_dest_uri);
693       if (status)
694         {
695           ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
696                         "Invalid URI in Destination header");
697           return HTTP_BAD_REQUEST;
698         }
699       if (!parsed_dest_uri.path)
700         {
701           ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
702                         "Invalid URI in Destination header");
703           return HTTP_BAD_REQUEST;
704         }
705 
706       ap_unescape_url(parsed_dest_uri.path);
707       dest_uri = parsed_dest_uri.path;
708       if (strncmp(dest_uri, conf->base_path, strlen(conf->base_path)))
709         {
710           /* If it is not the same location, then we don't allow it.
711            * XXX: Instead we could compare repository uuids, but that
712            * XXX: seems a bit over the top.
713            */
714           return HTTP_BAD_REQUEST;
715         }
716 
717       dav_err = dav_svn_split_uri(r,
718                                   dest_uri,
719                                   conf->base_path,
720                                   &cleaned_uri,
721                                   &trailing_slash,
722                                   &dest_repos_name,
723                                   &relative_path,
724                                   &dest_repos_path);
725 
726       if (dav_err)
727         {
728           ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
729                         "%s  [%d, #%d]",
730                         dav_err->desc, dav_err->status, dav_err->error_id);
731           /* Ensure that we never allow access by dav_err->status */
732           return (dav_err->status != OK && dav_err->status != DECLINED) ?
733                   dav_err->status : HTTP_INTERNAL_SERVER_ERROR;
734         }
735 
736       if (dest_repos_path)
737         dest_repos_path = svn_fspath__canonicalize(dest_repos_path, r->pool);
738 
739       *dest_repos_path_ref = apr_pstrcat(r->pool, dest_repos_name, ":",
740                                          dest_repos_path, SVN_VA_NULL);
741     }
742 
743   /* Retrieve/cache authorization file */
744   access_conf = get_access_conf(r,conf, r->pool);
745   if (access_conf == NULL)
746     return DECLINED;
747 
748   /* Perform authz access control.
749    *
750    * First test the special case where repos_path == NULL, and skip
751    * calling the authz routines in that case.  This is an oddity of
752    * the DAV RA method: some requests have no repos_path, but apache
753    * still triggers an authz lookup for the URI.
754    *
755    * However, if repos_path == NULL and the request requires write
756    * access, then perform a global authz lookup.  The request is
757    * denied if the user commiting isn't granted any access anywhere
758    * in the repository.  This is to avoid operations that involve no
759    * paths (commiting an empty revision, leaving a dangling
760    * transaction in the FS) being granted by default, letting
761    * unauthenticated users write some changes to the repository.
762    * This was issue #2388.
763    *
764    * XXX: For now, requesting access to the entire repository always
765    * XXX: succeeds, until we come up with a good way of figuring
766    * XXX: this out.
767    */
768   if (repos_path
769       || (!repos_path && (authz_svn_type & svn_authz_write)))
770     {
771       svn_err = svn_repos_authz_check_access(access_conf, repos_name,
772                                              repos_path,
773                                              username_to_authorize,
774                                              authz_svn_type,
775                                              &authz_access_granted,
776                                              r->pool);
777       if (svn_err)
778         {
779           log_svn_error(APLOG_MARK, r,
780                         "Failed to perform access control:",
781                         svn_err, r->pool);
782 
783           return DECLINED;
784         }
785         if (!authz_access_granted)
786           return DECLINED;
787     }
788 
789   /* XXX: MKCOL, MOVE, DELETE
790    * XXX: Require write access to the parent dir of repos_path.
791    */
792 
793   /* XXX: PUT
794    * XXX: If the path doesn't exist, require write access to the
795    * XXX: parent dir of repos_path.
796    */
797 
798   /* Only MOVE and COPY have a second uri we have to check access to. */
799   if (r->method_number != M_MOVE && r->method_number != M_COPY)
800     return OK;
801 
802   /* Check access on the destination repos_path.  Again, skip this if
803      repos_path == NULL (see above for explanations) */
804   if (repos_path)
805     {
806       svn_err = svn_repos_authz_check_access(access_conf,
807                                              dest_repos_name,
808                                              dest_repos_path,
809                                              username_to_authorize,
810                                              svn_authz_write
811                                              |svn_authz_recursive,
812                                              &authz_access_granted,
813                                              r->pool);
814       if (svn_err)
815         {
816           log_svn_error(APLOG_MARK, r,
817                         "Failed to perform access control:",
818                         svn_err, r->pool);
819 
820           return DECLINED;
821         }
822       if (!authz_access_granted)
823         return DECLINED;
824     }
825 
826   /* XXX: MOVE and COPY, if the path doesn't exist yet, also
827    * XXX: require write access to the parent dir of dest_repos_path.
828    */
829 
830   return OK;
831 }
832 
833 /*
834  * Implementation of subreq_bypass with scratch_pool parameter.
835  */
836 static int
subreq_bypass2(request_rec * r,const char * repos_path,const char * repos_name,apr_pool_t * scratch_pool)837 subreq_bypass2(request_rec *r,
838                const char *repos_path,
839                const char *repos_name,
840                apr_pool_t *scratch_pool)
841 {
842   svn_error_t *svn_err = NULL;
843   svn_authz_t *access_conf = NULL;
844   authz_svn_config_rec *conf = NULL;
845   svn_boolean_t authz_access_granted = FALSE;
846   const char *username_to_authorize;
847 
848   conf = ap_get_module_config(r->per_dir_config,
849                               &authz_svn_module);
850   username_to_authorize = get_username_to_authorize(r, conf, scratch_pool);
851 
852   /* If configured properly, this should never be true, but just in case. */
853   if (!conf->anonymous
854       || (! (conf->access_file || conf->repo_relative_access_file)))
855     {
856       log_access_verdict(APLOG_MARK, r, 0, TRUE, repos_path, NULL);
857       return HTTP_FORBIDDEN;
858     }
859 
860   /* Retrieve authorization file */
861   access_conf = get_access_conf(r, conf, scratch_pool);
862   if (access_conf == NULL)
863     return HTTP_FORBIDDEN;
864 
865   /* Perform authz access control.
866    * See similarly labeled comment in req_check_access.
867    */
868   if (repos_path)
869     {
870       svn_err = svn_repos_authz_check_access(access_conf, repos_name,
871                                              repos_path,
872                                              username_to_authorize,
873                                              svn_authz_none|svn_authz_read,
874                                              &authz_access_granted,
875                                              scratch_pool);
876       if (svn_err)
877         {
878           log_svn_error(APLOG_MARK, r,
879                         "Failed to perform access control:",
880                         svn_err, scratch_pool);
881           return HTTP_FORBIDDEN;
882         }
883       if (!authz_access_granted)
884         {
885           log_access_verdict(APLOG_MARK, r, 0, TRUE, repos_path, NULL);
886           return HTTP_FORBIDDEN;
887         }
888     }
889 
890   log_access_verdict(APLOG_MARK, r, 1, TRUE, repos_path, NULL);
891 
892   return OK;
893 }
894 
895 /*
896  * This function is used as a provider to allow mod_dav_svn to bypass the
897  * generation of an apache request when checking GET access from
898  * "mod_dav_svn/authz.c" .
899  */
900 static int
subreq_bypass(request_rec * r,const char * repos_path,const char * repos_name)901 subreq_bypass(request_rec *r,
902               const char *repos_path,
903               const char *repos_name)
904 {
905   int status;
906   apr_pool_t *scratch_pool;
907 
908   scratch_pool = svn_pool_create(r->pool);
909   status = subreq_bypass2(r, repos_path, repos_name, scratch_pool);
910   svn_pool_destroy(scratch_pool);
911 
912   return status;
913 }
914 
915 /*
916  * Hooks
917  */
918 
919 static int
access_checker(request_rec * r)920 access_checker(request_rec *r)
921 {
922   authz_svn_config_rec *conf = ap_get_module_config(r->per_dir_config,
923                                                     &authz_svn_module);
924   const char *repos_path = NULL;
925   const char *dest_repos_path = NULL;
926   int status, authn_required;
927 
928 #if USE_FORCE_AUTHN
929   /* Use the force_authn() hook available in 2.4.x to work securely
930    * given that ap_some_auth_required() is no longer functional for our
931    * purposes in 2.4.x.
932    */
933   int authn_configured;
934 
935   /* We are not configured to run */
936   if (!conf->anonymous || apr_table_get(r->notes, IN_SOME_AUTHN_NOTE)
937       || (! (conf->access_file || conf->repo_relative_access_file)))
938     return DECLINED;
939 
940   /* Authentication is configured */
941   authn_configured = ap_auth_type(r) != NULL;
942   if (authn_configured)
943     {
944       /* If the user is trying to authenticate, let him.  It doesn't
945        * make much sense to grant anonymous access but deny authenticated
946        * users access, even though you can do that with '$anon' in the
947        * access file.
948        */
949       if (apr_table_get(r->headers_in,
950                         (PROXYREQ_PROXY == r->proxyreq)
951                         ? "Proxy-Authorization" : "Authorization"))
952         {
953           /* Set the note to force authn regardless of what access_checker_ex
954              hook requires */
955           apr_table_setn(r->notes, FORCE_AUTHN_NOTE, "1");
956 
957           /* provide the proper return so the access_checker hook doesn't
958            * prevent the code from continuing on to the other auth hooks */
959           if (ap_satisfies(r) != SATISFY_ANY)
960             return OK;
961           else
962             return HTTP_FORBIDDEN;
963         }
964     }
965 
966 #else
967   /* Support for older versions of httpd that have a working
968    * ap_some_auth_required() */
969 
970   /* We are not configured to run */
971   if (!conf->anonymous
972       || (! (conf->access_file || conf->repo_relative_access_file)))
973     return DECLINED;
974 
975   authn_required = ap_some_auth_required(r);
976   if (authn_required)
977     {
978       /* It makes no sense to check if a location is both accessible
979        * anonymous and by an authenticated user (in the same request!).
980        */
981       if (ap_satisfies(r) != SATISFY_ANY)
982         return DECLINED;
983 
984       /* If the user is trying to authenticate, let him.  It doesn't
985        * make much sense to grant anonymous access but deny authenticated
986        * users access, even though you can do that with '$anon' in the
987        * access file.
988        */
989       if (apr_table_get(r->headers_in,
990                         (PROXYREQ_PROXY == r->proxyreq)
991                         ? "Proxy-Authorization" : "Authorization"))
992         {
993           /* Given Satisfy Any is in effect, we have to forbid access
994            * to let the auth_checker hook have a go at it.
995            */
996           return HTTP_FORBIDDEN;
997         }
998     }
999 #endif
1000 
1001   /* If anon access is allowed, return OK */
1002   status = req_check_access(r, conf, &repos_path, &dest_repos_path);
1003   if (status == DECLINED)
1004     {
1005       if (!conf->authoritative)
1006         return DECLINED;
1007 
1008 #if USE_FORCE_AUTHN
1009       if (authn_configured) {
1010           /* We have to check to see if authn is required because if so we must
1011            * return DECLINED rather than FORBIDDEN (403) since returning
1012            * the 403 leaks information about what paths may exist to
1013            * unauthenticated users.  Returning DECLINED means apache's request
1014            * handling will continue until the authn module itself generates
1015            * UNAUTHORIZED (401).
1016 
1017            * We must set a note here in order to use
1018            * ap_some_authn_rquired() without triggering an infinite
1019            * loop since the call will trigger this function to be
1020            * called again. */
1021           apr_table_setn(r->notes, IN_SOME_AUTHN_NOTE, "1");
1022           authn_required = ap_some_authn_required(r);
1023           apr_table_unset(r->notes, IN_SOME_AUTHN_NOTE);
1024           if (authn_required)
1025             return DECLINED;
1026       }
1027 #else
1028       if (!authn_required)
1029 #endif
1030         log_access_verdict(APLOG_MARK, r, 0, FALSE, repos_path, dest_repos_path);
1031 
1032       return HTTP_FORBIDDEN;
1033     }
1034 
1035   if (status != OK)
1036     return status;
1037 
1038   log_access_verdict(APLOG_MARK, r, 1, FALSE, repos_path, dest_repos_path);
1039 
1040   return OK;
1041 }
1042 
1043 static int
check_user_id(request_rec * r)1044 check_user_id(request_rec *r)
1045 {
1046   authz_svn_config_rec *conf = ap_get_module_config(r->per_dir_config,
1047                                                     &authz_svn_module);
1048   const char *repos_path = NULL;
1049   const char *dest_repos_path = NULL;
1050   int status;
1051 
1052   /* We are not configured to run, or, an earlier module has already
1053    * authenticated this request. */
1054   if (!conf->no_auth_when_anon_ok || r->user
1055       || (! (conf->access_file || conf->repo_relative_access_file)))
1056     return DECLINED;
1057 
1058   /* If anon access is allowed, return OK, preventing later modules
1059    * from issuing an HTTP_UNAUTHORIZED.  Also pass a note to our
1060    * auth_checker hook that access has already been checked. */
1061   status = req_check_access(r, conf, &repos_path, &dest_repos_path);
1062   if (status == OK)
1063     {
1064       apr_table_setn(r->notes, "authz_svn-anon-ok", "1");
1065       log_access_verdict(APLOG_MARK, r, 1, FALSE, repos_path, dest_repos_path);
1066       return OK;
1067     }
1068 
1069   return status;
1070 }
1071 
1072 static int
auth_checker(request_rec * r)1073 auth_checker(request_rec *r)
1074 {
1075   authz_svn_config_rec *conf = ap_get_module_config(r->per_dir_config,
1076                                                     &authz_svn_module);
1077   const char *repos_path = NULL;
1078   const char *dest_repos_path = NULL;
1079   int status;
1080 
1081   /* We are not configured to run */
1082   if (! (conf->access_file || conf->repo_relative_access_file))
1083     return DECLINED;
1084 
1085   /* Previous hook (check_user_id) already did all the work,
1086    * and, as a sanity check, r->user hasn't been set since then? */
1087   if (!r->user && apr_table_get(r->notes, "authz_svn-anon-ok"))
1088     return OK;
1089 
1090   status = req_check_access(r, conf, &repos_path, &dest_repos_path);
1091   if (status == DECLINED)
1092     {
1093       if (conf->authoritative)
1094         {
1095           log_access_verdict(APLOG_MARK, r, 0, FALSE, repos_path, dest_repos_path);
1096           ap_note_auth_failure(r);
1097           return HTTP_FORBIDDEN;
1098         }
1099       return DECLINED;
1100     }
1101 
1102   if (status != OK)
1103     return status;
1104 
1105   log_access_verdict(APLOG_MARK, r, 1, FALSE, repos_path, dest_repos_path);
1106 
1107   return OK;
1108 }
1109 
1110 #if USE_FORCE_AUTHN
1111 static int
force_authn(request_rec * r)1112 force_authn(request_rec *r)
1113 {
1114   if (apr_table_get(r->notes, FORCE_AUTHN_NOTE))
1115     return OK;
1116 
1117   return DECLINED;
1118 }
1119 #endif
1120 
1121 /*
1122  * Module flesh
1123  */
1124 
1125 /* Implements the #register_hooks method of Apache's #module vtable. */
1126 static void
register_hooks(apr_pool_t * p)1127 register_hooks(apr_pool_t *p)
1128 {
1129   static const char * const mod_ssl[] = { "mod_ssl.c", NULL };
1130 
1131   ap_hook_access_checker(access_checker, NULL, NULL, APR_HOOK_LAST);
1132   /* Our check_user_id hook must be before any module which will return
1133    * HTTP_UNAUTHORIZED (mod_auth_basic, etc.), but after mod_ssl, to
1134    * give SSLOptions +FakeBasicAuth a chance to work. */
1135   ap_hook_check_user_id(check_user_id, mod_ssl, NULL, APR_HOOK_FIRST);
1136   ap_hook_auth_checker(auth_checker, NULL, NULL, APR_HOOK_FIRST);
1137 #if USE_FORCE_AUTHN
1138   ap_hook_force_authn(force_authn, NULL, NULL, APR_HOOK_FIRST);
1139 #endif
1140   ap_register_provider(p,
1141                        AUTHZ_SVN__SUBREQ_BYPASS_PROV_GRP,
1142                        AUTHZ_SVN__SUBREQ_BYPASS_PROV_NAME,
1143                        AUTHZ_SVN__SUBREQ_BYPASS_PROV_VER,
1144                        (void*)subreq_bypass);
1145 }
1146 
1147 module AP_MODULE_DECLARE_DATA authz_svn_module =
1148 {
1149   STANDARD20_MODULE_STUFF,
1150   create_authz_svn_dir_config,     /* dir config creater */
1151   NULL,                            /* dir merger --- default is to override */
1152   NULL,                            /* server config */
1153   NULL,                            /* merge server config */
1154   authz_svn_cmds,                  /* command apr_table_t */
1155   register_hooks                   /* register hooks */
1156 };
1157