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-2005 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>
33 #include "mod_dav_svn.h"
34 #include "mod_authz_svn.h"
36 #include "svn_config.h"
37 #include "svn_string.h"
38 #include "svn_repos.h"
41 extern module AP_MODULE_DECLARE_DATA authz_svn_module
;
46 int no_auth_when_anon_ok
;
47 const char *base_path
;
48 const char *access_file
;
49 } authz_svn_config_rec
;
55 static void *create_authz_svn_dir_config(apr_pool_t
*p
, char *d
)
57 authz_svn_config_rec
*conf
= apr_pcalloc(p
, sizeof(*conf
));
60 /* By default keep the fortress secure */
61 conf
->authoritative
= 1;
67 static const command_rec authz_svn_cmds
[] =
69 AP_INIT_FLAG("AuthzSVNAuthoritative", ap_set_flag_slot
,
70 (void *)APR_OFFSETOF(authz_svn_config_rec
, authoritative
),
72 "Set to 'Off' to allow access control to be passed along to "
73 "lower modules. (default is On.)"),
74 AP_INIT_TAKE1("AuthzSVNAccessFile", ap_set_file_slot
,
75 (void *)APR_OFFSETOF(authz_svn_config_rec
, access_file
),
77 "Text file containing permissions of repository paths."),
78 AP_INIT_FLAG("AuthzSVNAnonymous", ap_set_flag_slot
,
79 (void *)APR_OFFSETOF(authz_svn_config_rec
, anonymous
),
81 "Set to 'Off' to disable two special-case behaviours of "
82 "this module: (1) interaction with the 'Satisfy Any' "
83 "directive, and (2) enforcement of the authorization "
84 "policy even when no 'Require' directives are present. "
86 AP_INIT_FLAG("AuthzSVNNoAuthWhenAnonymousAllowed", ap_set_flag_slot
,
87 (void *)APR_OFFSETOF(authz_svn_config_rec
,
88 no_auth_when_anon_ok
),
90 "Set to 'On' to suppress authentication and authorization "
91 "for requests which anonymous users are allowed to perform. "
97 * Get the, possibly cached, svn_authz_t for this request.
99 static svn_authz_t
*get_access_conf(request_rec
*r
,
100 authz_svn_config_rec
*conf
)
102 const char *cache_key
= NULL
;
103 void *user_data
= NULL
;
104 svn_authz_t
*access_conf
= NULL
;
105 svn_error_t
*svn_err
;
108 cache_key
= apr_pstrcat(r
->pool
, "mod_authz_svn:",
109 conf
->access_file
, NULL
);
110 apr_pool_userdata_get(&user_data
, cache_key
, r
->connection
->pool
);
111 access_conf
= user_data
;
112 if (access_conf
== NULL
) {
113 svn_err
= svn_repos_authz_read(&access_conf
, conf
->access_file
,
114 TRUE
, r
->connection
->pool
);
116 ap_log_rerror(APLOG_MARK
, APLOG_ERR
,
117 /* If it is an error code that APR can make sense
118 of, then show it, otherwise, pass zero to avoid
119 putting "APR does not understand this error code"
121 ((svn_err
->apr_err
>= APR_OS_START_USERERR
&&
122 svn_err
->apr_err
< APR_OS_START_CANONERR
) ?
123 0 : svn_err
->apr_err
),
124 r
, "Failed to load the AuthzSVNAccessFile: %s",
125 svn_err_best_message(svn_err
,
126 errbuf
, sizeof(errbuf
)));
127 svn_error_clear(svn_err
);
130 /* Cache the open repos for the next request on this connection */
131 apr_pool_userdata_set(access_conf
, cache_key
,
132 NULL
, r
->connection
->pool
);
138 /* Check if the current request R is allowed. Upon exit *REPOS_PATH_REF
139 * will contain the path and repository name that an operation was requested
140 * on in the form 'name:path'. *DEST_REPOS_PATH_REF will contain the
141 * destination path if the requested operation was a MOVE or a COPY.
142 * Returns OK when access is allowed, DECLINED when it isn't, or an HTTP_
143 * error code when an error occurred.
145 static int req_check_access(request_rec
*r
,
146 authz_svn_config_rec
*conf
,
147 const char **repos_path_ref
,
148 const char **dest_repos_path_ref
)
150 const char *dest_uri
;
151 apr_uri_t parsed_dest_uri
;
152 const char *cleaned_uri
;
154 const char *repos_name
;
155 const char *dest_repos_name
;
156 const char *relative_path
;
157 const char *repos_path
;
158 const char *dest_repos_path
= NULL
;
160 svn_repos_authz_access_t authz_svn_type
= svn_authz_none
;
161 svn_boolean_t authz_access_granted
= FALSE
;
162 svn_authz_t
*access_conf
= NULL
;
163 svn_error_t
*svn_err
;
166 switch (r
->method_number
) {
167 /* All methods requiring read access to all subtrees of r->uri */
169 authz_svn_type
|= svn_authz_recursive
;
171 /* All methods requiring read access to r->uri */
176 authz_svn_type
|= svn_authz_read
;
179 /* All methods requiring write access to all subtrees of r->uri */
182 authz_svn_type
|= svn_authz_recursive
;
184 /* All methods requiring write access to r->uri */
193 authz_svn_type
|= svn_authz_write
;
197 /* Require most strict access for unknown methods */
198 authz_svn_type
|= svn_authz_write
| svn_authz_recursive
;
202 dav_err
= dav_svn_split_uri(r
,
211 ap_log_rerror(APLOG_MARK
, APLOG_ERR
, 0, r
,
213 dav_err
->desc
, dav_err
->status
, dav_err
->error_id
);
214 /* Ensure that we never allow access by dav_err->status */
215 return (dav_err
->status
!= OK
&& dav_err
->status
!= DECLINED
) ?
216 dav_err
->status
: HTTP_INTERNAL_SERVER_ERROR
;
219 /* Ignore the URI passed to MERGE, like mod_dav_svn does.
221 * XXX: When we start accepting a broader range of DeltaV MERGE
222 * XXX: requests, this should be revisited.
224 if (r
->method_number
== M_MERGE
) {
229 repos_path
= svn_path_join("/", repos_path
, r
->pool
);
231 *repos_path_ref
= apr_pstrcat(r
->pool
, repos_name
, ":", repos_path
, NULL
);
233 if (r
->method_number
== M_MOVE
|| r
->method_number
== M_COPY
) {
234 dest_uri
= apr_table_get(r
->headers_in
, "Destination");
236 /* Decline MOVE or COPY when there is no Destination uri, this will
242 apr_uri_parse(r
->pool
, dest_uri
, &parsed_dest_uri
);
244 ap_unescape_url(parsed_dest_uri
.path
);
245 dest_uri
= parsed_dest_uri
.path
;
246 if (strncmp(dest_uri
, conf
->base_path
, strlen(conf
->base_path
))) {
247 /* If it is not the same location, then we don't allow it.
248 * XXX: Instead we could compare repository uuids, but that
249 * XXX: seems a bit over the top.
251 return HTTP_BAD_REQUEST
;
254 dav_err
= dav_svn_split_uri(r
,
264 ap_log_rerror(APLOG_MARK
, APLOG_ERR
, 0, r
,
266 dav_err
->desc
, dav_err
->status
, dav_err
->error_id
);
267 /* Ensure that we never allow access by dav_err->status */
268 return (dav_err
->status
!= OK
&& dav_err
->status
!= DECLINED
) ?
269 dav_err
->status
: HTTP_INTERNAL_SERVER_ERROR
;
273 dest_repos_path
= svn_path_join("/", dest_repos_path
, r
->pool
);
275 *dest_repos_path_ref
= apr_pstrcat(r
->pool
, dest_repos_name
, ":",
276 dest_repos_path
, NULL
);
279 /* Retrieve/cache authorization file */
280 access_conf
= get_access_conf(r
,conf
);
281 if (access_conf
== NULL
)
286 /* Perform authz access control.
288 * First test the special case where repos_path == NULL, and skip
289 * calling the authz routines in that case. This is an oddity of
290 * the DAV RA method: some requests have no repos_path, but apache
291 * still triggers an authz lookup for the URI.
293 * However, if repos_path == NULL and the request requires write
294 * access, then perform a global authz lookup. The request is
295 * denied if the user commiting isn't granted any access anywhere
296 * in the repository. This is to avoid operations that involve no
297 * paths (commiting an empty revision, leaving a dangling
298 * transaction in the FS) being granted by default, letting
299 * unauthenticated users write some changes to the repository.
300 * This was issue #2388.
302 * XXX: For now, requesting access to the entire repository always
303 * XXX: succeeds, until we come up with a good way of figuring
307 || (!repos_path
&& (authz_svn_type
& svn_authz_write
)))
309 svn_err
= svn_repos_authz_check_access(access_conf
, repos_name
,
312 &authz_access_granted
,
315 ap_log_rerror(APLOG_MARK
, APLOG_ERR
,
316 /* If it is an error code that APR can make
317 sense of, then show it, otherwise, pass
318 zero to avoid putting "APR does not
319 understand this error code" in the error
321 ((svn_err
->apr_err
>= APR_OS_START_USERERR
&&
322 svn_err
->apr_err
< APR_OS_START_CANONERR
) ?
323 0 : svn_err
->apr_err
),
324 r
, "Failed to perform access control: %s",
325 svn_err_best_message(svn_err
, errbuf
, sizeof(errbuf
)));
326 svn_error_clear(svn_err
);
330 if (!authz_access_granted
)
334 /* XXX: MKCOL, MOVE, DELETE
335 * XXX: Require write access to the parent dir of repos_path.
339 * XXX: If the path doesn't exist, require write access to the
340 * XXX: parent dir of repos_path.
343 /* Only MOVE and COPY have a second uri we have to check access to. */
344 if (r
->method_number
!= M_MOVE
345 && r
->method_number
!= M_COPY
) {
349 /* Check access on the destination repos_path. Again, skip this if
350 repos_path == NULL (see above for explanations) */
353 svn_err
= svn_repos_authz_check_access(access_conf
,
358 |svn_authz_recursive
,
359 &authz_access_granted
,
362 ap_log_rerror(APLOG_MARK
, APLOG_ERR
,
363 /* If it is an error code that APR can make sense
364 of, then show it, otherwise, pass zero to avoid
365 putting "APR does not understand this error code"
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
, sizeof(errbuf
)));
372 svn_error_clear(svn_err
);
376 if (!authz_access_granted
)
380 /* XXX: MOVE and COPY, if the path doesn't exist yet, also
381 * XXX: require write access to the parent dir of dest_repos_path.
387 /* Log a message indicating the access control decision made about a
388 * request. FILE and LINE should be supplied via the APLOG_MARK macro.
389 * ALLOWED is boolean. REPOS_PATH and DEST_REPOS_PATH are information
390 * about the request. DEST_REPOS_PATH may be NULL. */
391 static void log_access_verdict(const char *file
, int line
,
392 const request_rec
*r
,
394 const char *repos_path
,
395 const char *dest_repos_path
)
397 int level
= allowed
? APLOG_INFO
: APLOG_ERR
;
398 const char *verdict
= allowed
? "granted" : "denied";
401 if (dest_repos_path
) {
402 ap_log_rerror(file
, line
, level
, 0, r
,
403 "Access %s: '%s' %s %s %s", verdict
, r
->user
,
404 r
->method
, repos_path
, dest_repos_path
);
407 ap_log_rerror(file
, line
, level
, 0, r
,
408 "Access %s: '%s' %s %s", verdict
, r
->user
,
409 r
->method
, repos_path
);
413 if (dest_repos_path
) {
414 ap_log_rerror(file
, line
, level
, 0, r
,
415 "Access %s: - %s %s %s", verdict
,
416 r
->method
, repos_path
, dest_repos_path
);
419 ap_log_rerror(file
, line
, level
, 0, r
,
420 "Access %s: - %s %s", verdict
,
421 r
->method
, repos_path
);
427 * This function is used as a provider to allow mod_dav_svn to bypass the
428 * generation of an apache request when checking GET access from
429 * "mod_dav_svn/authz.c" .
431 static int subreq_bypass(request_rec
*r
,
432 const char *repos_path
,
433 const char *repos_name
)
435 svn_error_t
*svn_err
= NULL
;
436 svn_authz_t
*access_conf
= NULL
;
437 authz_svn_config_rec
*conf
= NULL
;
438 svn_boolean_t authz_access_granted
= FALSE
;
441 conf
= ap_get_module_config(r
->per_dir_config
,
444 /* If configured properly, this should never be true, but just in case. */
445 if (!conf
->anonymous
|| !conf
->access_file
) {
446 log_access_verdict(APLOG_MARK
, r
, 0, repos_path
, NULL
);
447 return HTTP_FORBIDDEN
;
450 /* Retrieve authorization file */
451 access_conf
= get_access_conf(r
,conf
);
452 if (access_conf
== NULL
)
453 return HTTP_FORBIDDEN
;
455 /* Perform authz access control.
456 * See similarly labeled comment in req_check_access.
459 svn_err
= svn_repos_authz_check_access(access_conf
, repos_name
,
461 svn_authz_none
|svn_authz_read
,
462 &authz_access_granted
,
465 ap_log_rerror(APLOG_MARK
, APLOG_ERR
,
466 /* If it is an error code that APR can make
467 sense of, then show it, otherwise, pass
468 zero to avoid putting "APR does not
469 understand this error code" in the error
471 ((svn_err
->apr_err
>= APR_OS_START_USERERR
&&
472 svn_err
->apr_err
< APR_OS_START_CANONERR
) ?
473 0 : svn_err
->apr_err
),
474 r
, "Failed to perform access control: %s",
475 svn_err_best_message(svn_err
, errbuf
, sizeof(errbuf
)));
476 svn_error_clear(svn_err
);
477 return HTTP_FORBIDDEN
;
479 if (!authz_access_granted
) {
480 log_access_verdict(APLOG_MARK
, r
, 0, repos_path
, NULL
);
481 return HTTP_FORBIDDEN
;
485 log_access_verdict(APLOG_MARK
, r
, 1, repos_path
, NULL
);
494 static int access_checker(request_rec
*r
)
496 authz_svn_config_rec
*conf
= ap_get_module_config(r
->per_dir_config
,
498 const char *repos_path
;
499 const char *dest_repos_path
= NULL
;
502 /* We are not configured to run */
503 if (!conf
->anonymous
|| !conf
->access_file
)
506 if (ap_some_auth_required(r
)) {
507 /* It makes no sense to check if a location is both accessible
508 * anonymous and by an authenticated user (in the same request!).
510 if (ap_satisfies(r
) != SATISFY_ANY
)
513 /* If the user is trying to authenticate, let him. If anonymous
514 * access is allowed, so is authenticated access, by definition
515 * of the meaning of '*' in the access file.
517 if (apr_table_get(r
->headers_in
,
518 (PROXYREQ_PROXY
== r
->proxyreq
)
519 ? "Proxy-Authorization" : "Authorization")) {
520 /* Given Satisfy Any is in effect, we have to forbid access
521 * to let the auth_checker hook have a go at it.
523 return HTTP_FORBIDDEN
;
527 /* If anon access is allowed, return OK */
528 status
= req_check_access(r
, conf
, &repos_path
, &dest_repos_path
);
529 if (status
== DECLINED
) {
530 if (!conf
->authoritative
)
533 if (!ap_some_auth_required(r
)) {
534 log_access_verdict(APLOG_MARK
, r
, 0, repos_path
, dest_repos_path
);
537 return HTTP_FORBIDDEN
;
543 log_access_verdict(APLOG_MARK
, r
, 1, repos_path
, dest_repos_path
);
548 static int check_user_id(request_rec
*r
)
550 authz_svn_config_rec
*conf
= ap_get_module_config(r
->per_dir_config
,
552 const char *repos_path
;
553 const char *dest_repos_path
= NULL
;
556 /* We are not configured to run, or, an earlier module has already
557 * authenticated this request. */
558 if (!conf
->access_file
|| !conf
->no_auth_when_anon_ok
|| r
->user
)
561 /* If anon access is allowed, return OK, preventing later modules
562 * from issuing an HTTP_UNAUTHORIZED. Also pass a note to our
563 * auth_checker hook that access has already been checked. */
564 status
= req_check_access(r
, conf
, &repos_path
, &dest_repos_path
);
566 apr_table_setn(r
->notes
, "authz_svn-anon-ok", (const char*)1);
567 log_access_verdict(APLOG_MARK
, r
, 1, repos_path
, dest_repos_path
);
574 static int auth_checker(request_rec
*r
)
576 authz_svn_config_rec
*conf
= ap_get_module_config(r
->per_dir_config
,
578 const char *repos_path
;
579 const char *dest_repos_path
= NULL
;
582 /* We are not configured to run */
583 if (!conf
->access_file
)
586 /* Previous hook (check_user_id) already did all the work,
587 * and, as a sanity check, r->user hasn't been set since then? */
588 if (!r
->user
&& apr_table_get(r
->notes
, "authz_svn-anon-ok")) {
592 status
= req_check_access(r
, conf
, &repos_path
, &dest_repos_path
);
593 if (status
== DECLINED
) {
594 if (conf
->authoritative
) {
595 log_access_verdict(APLOG_MARK
, r
, 0, repos_path
, dest_repos_path
);
596 ap_note_auth_failure(r
);
597 return HTTP_FORBIDDEN
;
606 log_access_verdict(APLOG_MARK
, r
, 1, repos_path
, dest_repos_path
);
615 static void register_hooks(apr_pool_t
*p
)
617 static const char * const mod_ssl
[] = { "mod_ssl.c", NULL
};
619 ap_hook_access_checker(access_checker
, NULL
, NULL
, APR_HOOK_LAST
);
620 /* Our check_user_id hook must be before any module which will return
621 * HTTP_UNAUTHORIZED (mod_auth_basic, etc.), but after mod_ssl, to
622 * give SSLOptions +FakeBasicAuth a chance to work. */
623 ap_hook_check_user_id(check_user_id
, mod_ssl
, NULL
, APR_HOOK_FIRST
);
624 ap_hook_auth_checker(auth_checker
, NULL
, NULL
, APR_HOOK_FIRST
);
625 ap_register_provider(p
,
626 AUTHZ_SVN__SUBREQ_BYPASS_PROV_GRP
,
627 AUTHZ_SVN__SUBREQ_BYPASS_PROV_NAME
,
628 AUTHZ_SVN__SUBREQ_BYPASS_PROV_VER
,
629 (void*)subreq_bypass
);
632 module AP_MODULE_DECLARE_DATA authz_svn_module
=
634 STANDARD20_MODULE_STUFF
,
635 create_authz_svn_dir_config
, /* dir config creater */
636 NULL
, /* dir merger --- default is to override */
637 NULL
, /* server config */
638 NULL
, /* merge server config */
639 authz_svn_cmds
, /* command apr_table_t */
640 register_hooks
/* register hooks */