2 * mod_authz_svn.c: an Apache mod_dav_svn sub-module to provide path
3 * based authorization for a Subversion repository.
5 * ====================================================================
6 * Copyright (c) 2003-2008 CollabNet. All rights reserved.
8 * This software is licensed as described in the file COPYING, which
9 * you should have received as part of this distribution. The terms
10 * are also available at http://subversion.tigris.org/license-1.html.
11 * If newer versions of this license are posted there, you may use a
12 * newer version instead, at your option.
14 * This software consists of voluntary contributions made by many
15 * individuals. For exact contribution history, see the revision
16 * history and logs, available at http://subversion.tigris.org/.
17 * ====================================================================
23 #include <http_config.h>
24 #include <http_core.h>
25 #include <http_request.h>
26 #include <http_protocol.h>
28 #include <ap_config.h>
29 #include <ap_provider.h>
34 #include "mod_dav_svn.h"
35 #include "mod_authz_svn.h"
37 #include "svn_config.h"
38 #include "svn_string.h"
39 #include "svn_repos.h"
42 extern module AP_MODULE_DECLARE_DATA authz_svn_module
;
47 int no_auth_when_anon_ok
;
48 const char *base_path
;
49 const char *access_file
;
50 const char *force_username_case
;
51 } authz_svn_config_rec
;
58 create_authz_svn_dir_config(apr_pool_t
*p
, char *d
)
60 authz_svn_config_rec
*conf
= apr_pcalloc(p
, sizeof(*conf
));
63 /* By default keep the fortress secure */
64 conf
->authoritative
= 1;
70 static const command_rec authz_svn_cmds
[] =
72 AP_INIT_FLAG("AuthzSVNAuthoritative", ap_set_flag_slot
,
73 (void *)APR_OFFSETOF(authz_svn_config_rec
, authoritative
),
75 "Set to 'Off' to allow access control to be passed along to "
76 "lower modules. (default is On.)"),
77 AP_INIT_TAKE1("AuthzSVNAccessFile", ap_set_file_slot
,
78 (void *)APR_OFFSETOF(authz_svn_config_rec
, access_file
),
80 "Text file containing permissions of repository paths."),
81 AP_INIT_FLAG("AuthzSVNAnonymous", ap_set_flag_slot
,
82 (void *)APR_OFFSETOF(authz_svn_config_rec
, anonymous
),
84 "Set to 'Off' to disable two special-case behaviours of "
85 "this module: (1) interaction with the 'Satisfy Any' "
86 "directive, and (2) enforcement of the authorization "
87 "policy even when no 'Require' directives are present. "
89 AP_INIT_FLAG("AuthzSVNNoAuthWhenAnonymousAllowed", ap_set_flag_slot
,
90 (void *)APR_OFFSETOF(authz_svn_config_rec
,
91 no_auth_when_anon_ok
),
93 "Set to 'On' to suppress authentication and authorization "
94 "for requests which anonymous users are allowed to perform. "
96 AP_INIT_TAKE1("AuthzForceUsernameCase", ap_set_string_slot
,
97 (void *)APR_OFFSETOF(authz_svn_config_rec
,
100 "Set to 'Upper' or 'Lower' to convert the username before "
101 "checking for authorization."),
106 * Get the, possibly cached, svn_authz_t for this request.
109 get_access_conf(request_rec
*r
, authz_svn_config_rec
*conf
)
111 const char *cache_key
= NULL
;
112 void *user_data
= NULL
;
113 svn_authz_t
*access_conf
= NULL
;
114 svn_error_t
*svn_err
;
117 cache_key
= apr_pstrcat(r
->pool
, "mod_authz_svn:",
118 conf
->access_file
, NULL
);
119 apr_pool_userdata_get(&user_data
, cache_key
, r
->connection
->pool
);
120 access_conf
= user_data
;
121 if (access_conf
== NULL
)
123 svn_err
= svn_repos_authz_read(&access_conf
, conf
->access_file
,
124 TRUE
, r
->connection
->pool
);
127 ap_log_rerror(APLOG_MARK
, APLOG_ERR
,
128 /* If it is an error code that APR can make sense
129 of, then show it, otherwise, pass zero to avoid
130 putting "APR does not understand this error code"
132 ((svn_err
->apr_err
>= APR_OS_START_USERERR
&&
133 svn_err
->apr_err
< APR_OS_START_CANONERR
) ?
134 0 : svn_err
->apr_err
),
135 r
, "Failed to load the AuthzSVNAccessFile: %s",
136 svn_err_best_message(svn_err
, errbuf
, sizeof(errbuf
)));
137 svn_error_clear(svn_err
);
142 /* Cache the open repos for the next request on this connection */
143 apr_pool_userdata_set(access_conf
, cache_key
,
144 NULL
, r
->connection
->pool
);
150 /* Convert TEXT to upper case if TO_UPPERCASE is TRUE, else
151 converts it to lower case. */
153 convert_case(char *text
, svn_boolean_t to_uppercase
)
158 *c
= (to_uppercase
? apr_toupper(*c
) : apr_tolower(*c
));
163 /* Return the username to authorize, with case-conversion performed if
164 CONF->force_username_case is set. */
166 get_username_to_authorize(request_rec
*r
, authz_svn_config_rec
*conf
)
168 char *username_to_authorize
= r
->user
;
169 if (conf
->force_username_case
)
171 username_to_authorize
= apr_pstrdup(r
->pool
, r
->user
);
172 convert_case(username_to_authorize
,
173 strcasecmp(conf
->force_username_case
, "upper") == 0);
175 return username_to_authorize
;
178 /* Check if the current request R is allowed. Upon exit *REPOS_PATH_REF
179 * will contain the path and repository name that an operation was requested
180 * on in the form 'name:path'. *DEST_REPOS_PATH_REF will contain the
181 * destination path if the requested operation was a MOVE or a COPY.
182 * Returns OK when access is allowed, DECLINED when it isn't, or an HTTP_
183 * error code when an error occurred.
186 req_check_access(request_rec
*r
,
187 authz_svn_config_rec
*conf
,
188 const char **repos_path_ref
,
189 const char **dest_repos_path_ref
)
191 const char *dest_uri
;
192 apr_uri_t parsed_dest_uri
;
193 const char *cleaned_uri
;
195 const char *repos_name
;
196 const char *dest_repos_name
;
197 const char *relative_path
;
198 const char *repos_path
;
199 const char *dest_repos_path
= NULL
;
201 svn_repos_authz_access_t authz_svn_type
= svn_authz_none
;
202 svn_boolean_t authz_access_granted
= FALSE
;
203 svn_authz_t
*access_conf
= NULL
;
204 svn_error_t
*svn_err
;
206 const char *username_to_authorize
= get_username_to_authorize(r
, conf
);
208 switch (r
->method_number
)
210 /* All methods requiring read access to all subtrees of r->uri */
212 authz_svn_type
|= svn_authz_recursive
;
214 /* All methods requiring read access to r->uri */
219 authz_svn_type
|= svn_authz_read
;
222 /* All methods requiring write access to all subtrees of r->uri */
225 authz_svn_type
|= svn_authz_recursive
;
227 /* All methods requiring write access to r->uri */
236 authz_svn_type
|= svn_authz_write
;
240 /* Require most strict access for unknown methods */
241 authz_svn_type
|= svn_authz_write
| svn_authz_recursive
;
245 dav_err
= dav_svn_split_uri(r
,
255 ap_log_rerror(APLOG_MARK
, APLOG_ERR
, 0, r
,
257 dav_err
->desc
, dav_err
->status
, dav_err
->error_id
);
258 /* Ensure that we never allow access by dav_err->status */
259 return (dav_err
->status
!= OK
&& dav_err
->status
!= DECLINED
) ?
260 dav_err
->status
: HTTP_INTERNAL_SERVER_ERROR
;
263 /* Ignore the URI passed to MERGE, like mod_dav_svn does.
265 * XXX: When we start accepting a broader range of DeltaV MERGE
266 * XXX: requests, this should be revisited.
268 if (r
->method_number
== M_MERGE
)
272 repos_path
= svn_path_join("/", repos_path
, r
->pool
);
274 *repos_path_ref
= apr_pstrcat(r
->pool
, repos_name
, ":", repos_path
, NULL
);
276 if (r
->method_number
== M_MOVE
|| r
->method_number
== M_COPY
)
278 dest_uri
= apr_table_get(r
->headers_in
, "Destination");
280 /* Decline MOVE or COPY when there is no Destination uri, this will
286 apr_uri_parse(r
->pool
, dest_uri
, &parsed_dest_uri
);
288 ap_unescape_url(parsed_dest_uri
.path
);
289 dest_uri
= parsed_dest_uri
.path
;
290 if (strncmp(dest_uri
, conf
->base_path
, strlen(conf
->base_path
)))
292 /* If it is not the same location, then we don't allow it.
293 * XXX: Instead we could compare repository uuids, but that
294 * XXX: seems a bit over the top.
296 return HTTP_BAD_REQUEST
;
299 dav_err
= dav_svn_split_uri(r
,
310 ap_log_rerror(APLOG_MARK
, APLOG_ERR
, 0, r
,
312 dav_err
->desc
, dav_err
->status
, dav_err
->error_id
);
313 /* Ensure that we never allow access by dav_err->status */
314 return (dav_err
->status
!= OK
&& dav_err
->status
!= DECLINED
) ?
315 dav_err
->status
: HTTP_INTERNAL_SERVER_ERROR
;
319 dest_repos_path
= svn_path_join("/", dest_repos_path
, r
->pool
);
321 *dest_repos_path_ref
= apr_pstrcat(r
->pool
, dest_repos_name
, ":",
322 dest_repos_path
, NULL
);
325 /* Retrieve/cache authorization file */
326 access_conf
= get_access_conf(r
,conf
);
327 if (access_conf
== NULL
)
330 /* Perform authz access control.
332 * First test the special case where repos_path == NULL, and skip
333 * calling the authz routines in that case. This is an oddity of
334 * the DAV RA method: some requests have no repos_path, but apache
335 * still triggers an authz lookup for the URI.
337 * However, if repos_path == NULL and the request requires write
338 * access, then perform a global authz lookup. The request is
339 * denied if the user commiting isn't granted any access anywhere
340 * in the repository. This is to avoid operations that involve no
341 * paths (commiting an empty revision, leaving a dangling
342 * transaction in the FS) being granted by default, letting
343 * unauthenticated users write some changes to the repository.
344 * This was issue #2388.
346 * XXX: For now, requesting access to the entire repository always
347 * XXX: succeeds, until we come up with a good way of figuring
351 || (!repos_path
&& (authz_svn_type
& svn_authz_write
)))
353 svn_err
= svn_repos_authz_check_access(access_conf
, repos_name
,
355 username_to_authorize
,
357 &authz_access_granted
,
361 ap_log_rerror(APLOG_MARK
, APLOG_ERR
,
362 /* If it is an error code that APR can make
363 sense of, then show it, otherwise, pass
364 zero to avoid putting "APR does not
365 understand this error code" in the error
367 ((svn_err
->apr_err
>= APR_OS_START_USERERR
&&
368 svn_err
->apr_err
< APR_OS_START_CANONERR
) ?
369 0 : svn_err
->apr_err
),
370 r
, "Failed to perform access control: %s",
371 svn_err_best_message(svn_err
, errbuf
,
373 svn_error_clear(svn_err
);
377 if (!authz_access_granted
)
381 /* XXX: MKCOL, MOVE, DELETE
382 * XXX: Require write access to the parent dir of repos_path.
386 * XXX: If the path doesn't exist, require write access to the
387 * XXX: parent dir of repos_path.
390 /* Only MOVE and COPY have a second uri we have to check access to. */
391 if (r
->method_number
!= M_MOVE
&& r
->method_number
!= M_COPY
)
394 /* Check access on the destination repos_path. Again, skip this if
395 repos_path == NULL (see above for explanations) */
398 svn_err
= svn_repos_authz_check_access(access_conf
,
401 username_to_authorize
,
403 |svn_authz_recursive
,
404 &authz_access_granted
,
408 ap_log_rerror(APLOG_MARK
, APLOG_ERR
,
409 /* If it is an error code that APR can make sense
410 of, then show it, otherwise, pass zero to avoid
411 putting "APR does not understand this error code"
413 ((svn_err
->apr_err
>= APR_OS_START_USERERR
&&
414 svn_err
->apr_err
< APR_OS_START_CANONERR
) ?
415 0 : svn_err
->apr_err
),
416 r
, "Failed to perform access control: %s",
417 svn_err_best_message(svn_err
, errbuf
, sizeof(errbuf
)));
418 svn_error_clear(svn_err
);
422 if (!authz_access_granted
)
426 /* XXX: MOVE and COPY, if the path doesn't exist yet, also
427 * XXX: require write access to the parent dir of dest_repos_path.
433 /* Log a message indicating the access control decision made about a
434 * request. FILE and LINE should be supplied via the APLOG_MARK macro.
435 * ALLOWED is boolean. REPOS_PATH and DEST_REPOS_PATH are information
436 * about the request. DEST_REPOS_PATH may be NULL. */
438 log_access_verdict(const char *file
, int line
,
439 const request_rec
*r
, int allowed
,
440 const char *repos_path
, const char *dest_repos_path
)
442 int level
= allowed
? APLOG_INFO
: APLOG_ERR
;
443 const char *verdict
= allowed
? "granted" : "denied";
448 ap_log_rerror(file
, line
, level
, 0, r
,
449 "Access %s: '%s' %s %s %s", verdict
, r
->user
,
450 r
->method
, repos_path
, dest_repos_path
);
452 ap_log_rerror(file
, line
, level
, 0, r
,
453 "Access %s: '%s' %s %s", verdict
, r
->user
,
454 r
->method
, repos_path
);
459 ap_log_rerror(file
, line
, level
, 0, r
,
460 "Access %s: - %s %s %s", verdict
,
461 r
->method
, repos_path
, dest_repos_path
);
463 ap_log_rerror(file
, line
, level
, 0, r
,
464 "Access %s: - %s %s", verdict
,
465 r
->method
, repos_path
);
470 * This function is used as a provider to allow mod_dav_svn to bypass the
471 * generation of an apache request when checking GET access from
472 * "mod_dav_svn/authz.c" .
475 subreq_bypass(request_rec
*r
,
476 const char *repos_path
,
477 const char *repos_name
)
479 svn_error_t
*svn_err
= NULL
;
480 svn_authz_t
*access_conf
= NULL
;
481 authz_svn_config_rec
*conf
= NULL
;
482 svn_boolean_t authz_access_granted
= FALSE
;
484 const char *username_to_authorize
;
486 conf
= ap_get_module_config(r
->per_dir_config
,
488 username_to_authorize
= get_username_to_authorize(r
, conf
);
490 /* If configured properly, this should never be true, but just in case. */
491 if (!conf
->anonymous
|| !conf
->access_file
)
493 log_access_verdict(APLOG_MARK
, r
, 0, repos_path
, NULL
);
494 return HTTP_FORBIDDEN
;
497 /* Retrieve authorization file */
498 access_conf
= get_access_conf(r
,conf
);
499 if (access_conf
== NULL
)
500 return HTTP_FORBIDDEN
;
502 /* Perform authz access control.
503 * See similarly labeled comment in req_check_access.
507 svn_err
= svn_repos_authz_check_access(access_conf
, repos_name
,
509 username_to_authorize
,
510 svn_authz_none
|svn_authz_read
,
511 &authz_access_granted
,
515 ap_log_rerror(APLOG_MARK
, APLOG_ERR
,
516 /* If it is an error code that APR can make
517 sense of, then show it, otherwise, pass
518 zero to avoid putting "APR does not
519 understand this error code" in the error
521 ((svn_err
->apr_err
>= APR_OS_START_USERERR
&&
522 svn_err
->apr_err
< APR_OS_START_CANONERR
) ?
523 0 : svn_err
->apr_err
),
524 r
, "Failed to perform access control: %s",
525 svn_err_best_message(svn_err
, errbuf
, sizeof(errbuf
)));
526 svn_error_clear(svn_err
);
527 return HTTP_FORBIDDEN
;
529 if (!authz_access_granted
)
531 log_access_verdict(APLOG_MARK
, r
, 0, repos_path
, NULL
);
532 return HTTP_FORBIDDEN
;
536 log_access_verdict(APLOG_MARK
, r
, 1, repos_path
, NULL
);
546 access_checker(request_rec
*r
)
548 authz_svn_config_rec
*conf
= ap_get_module_config(r
->per_dir_config
,
550 const char *repos_path
;
551 const char *dest_repos_path
= NULL
;
554 /* We are not configured to run */
555 if (!conf
->anonymous
|| !conf
->access_file
)
558 if (ap_some_auth_required(r
))
560 /* It makes no sense to check if a location is both accessible
561 * anonymous and by an authenticated user (in the same request!).
563 if (ap_satisfies(r
) != SATISFY_ANY
)
566 /* If the user is trying to authenticate, let him. If anonymous
567 * access is allowed, so is authenticated access, by definition
568 * of the meaning of '*' in the access file.
570 if (apr_table_get(r
->headers_in
,
571 (PROXYREQ_PROXY
== r
->proxyreq
)
572 ? "Proxy-Authorization" : "Authorization"))
574 /* Given Satisfy Any is in effect, we have to forbid access
575 * to let the auth_checker hook have a go at it.
577 return HTTP_FORBIDDEN
;
581 /* If anon access is allowed, return OK */
582 status
= req_check_access(r
, conf
, &repos_path
, &dest_repos_path
);
583 if (status
== DECLINED
)
585 if (!conf
->authoritative
)
588 if (!ap_some_auth_required(r
))
589 log_access_verdict(APLOG_MARK
, r
, 0, repos_path
, dest_repos_path
);
591 return HTTP_FORBIDDEN
;
597 log_access_verdict(APLOG_MARK
, r
, 1, repos_path
, dest_repos_path
);
603 check_user_id(request_rec
*r
)
605 authz_svn_config_rec
*conf
= ap_get_module_config(r
->per_dir_config
,
607 const char *repos_path
;
608 const char *dest_repos_path
= NULL
;
611 /* We are not configured to run, or, an earlier module has already
612 * authenticated this request. */
613 if (!conf
->access_file
|| !conf
->no_auth_when_anon_ok
|| r
->user
)
616 /* If anon access is allowed, return OK, preventing later modules
617 * from issuing an HTTP_UNAUTHORIZED. Also pass a note to our
618 * auth_checker hook that access has already been checked. */
619 status
= req_check_access(r
, conf
, &repos_path
, &dest_repos_path
);
622 apr_table_setn(r
->notes
, "authz_svn-anon-ok", (const char*)1);
623 log_access_verdict(APLOG_MARK
, r
, 1, repos_path
, dest_repos_path
);
631 auth_checker(request_rec
*r
)
633 authz_svn_config_rec
*conf
= ap_get_module_config(r
->per_dir_config
,
635 const char *repos_path
;
636 const char *dest_repos_path
= NULL
;
639 /* We are not configured to run */
640 if (!conf
->access_file
)
643 /* Previous hook (check_user_id) already did all the work,
644 * and, as a sanity check, r->user hasn't been set since then? */
645 if (!r
->user
&& apr_table_get(r
->notes
, "authz_svn-anon-ok"))
648 status
= req_check_access(r
, conf
, &repos_path
, &dest_repos_path
);
649 if (status
== DECLINED
)
651 if (conf
->authoritative
)
653 log_access_verdict(APLOG_MARK
, r
, 0, repos_path
, dest_repos_path
);
654 ap_note_auth_failure(r
);
655 return HTTP_FORBIDDEN
;
663 log_access_verdict(APLOG_MARK
, r
, 1, repos_path
, dest_repos_path
);
673 register_hooks(apr_pool_t
*p
)
675 static const char * const mod_ssl
[] = { "mod_ssl.c", NULL
};
677 ap_hook_access_checker(access_checker
, NULL
, NULL
, APR_HOOK_LAST
);
678 /* Our check_user_id hook must be before any module which will return
679 * HTTP_UNAUTHORIZED (mod_auth_basic, etc.), but after mod_ssl, to
680 * give SSLOptions +FakeBasicAuth a chance to work. */
681 ap_hook_check_user_id(check_user_id
, mod_ssl
, NULL
, APR_HOOK_FIRST
);
682 ap_hook_auth_checker(auth_checker
, NULL
, NULL
, APR_HOOK_FIRST
);
683 ap_register_provider(p
,
684 AUTHZ_SVN__SUBREQ_BYPASS_PROV_GRP
,
685 AUTHZ_SVN__SUBREQ_BYPASS_PROV_NAME
,
686 AUTHZ_SVN__SUBREQ_BYPASS_PROV_VER
,
687 (void*)subreq_bypass
);
690 module AP_MODULE_DECLARE_DATA authz_svn_module
=
692 STANDARD20_MODULE_STUFF
,
693 create_authz_svn_dir_config
, /* dir config creater */
694 NULL
, /* dir merger --- default is to override */
695 NULL
, /* server config */
696 NULL
, /* merge server config */
697 authz_svn_cmds
, /* command apr_table_t */
698 register_hooks
/* register hooks */