/* * mod_authz_svn.c: an Apache mod_dav_svn sub-module to provide path * based authorization for a Subversion repository. * * ==================================================================== * Copyright (c) 2003-2005 CollabNet. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://subversion.tigris.org/license-1.html. * If newer versions of this license are posted there, you may use a * newer version instead, at your option. * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://subversion.tigris.org/. * ==================================================================== */ #include #include #include #include #include #include #include #include #include #if AP_MODULE_MAGIC_AT_LEAST(20060110,0) #include extern APR_OPTIONAL_FN_TYPE(ap_satisfies) *ap_satisfies; #endif #include "mod_dav_svn.h" #include "svn_path.h" #include "svn_config.h" #include "svn_string.h" #include "svn_repos.h" extern module AP_MODULE_DECLARE_DATA authz_svn_module; typedef struct { int authoritative; int anonymous; int no_auth_when_anon_ok; const char *base_path; const char *access_file; } authz_svn_config_rec; /* * Configuration */ 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; /* By default keep the fortress secure */ conf->authoritative = 1; conf->anonymous = 1; return conf; } 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", ap_set_file_slot, (void *)APR_OFFSETOF(authz_svn_config_rec, access_file), OR_AUTHCFG, "Text file containing permissions of repository paths."), 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.)"), { NULL } }; /* 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 *cache_key; void *user_data; char errbuf[256]; 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; } 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_path_join("/", repos_path, r->pool); *repos_path_ref = apr_pstrcat(r->pool, repos_name, ":", repos_path, 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_path_join("/", dest_repos_path, r->pool); *dest_repos_path_ref = apr_pstrcat(r->pool, dest_repos_name, ":", dest_repos_path, NULL); } /* Retrieve/cache authorization file */ cache_key = apr_pstrcat(r->pool, "mod_authz_svn:", conf->access_file, 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_read(&access_conf, conf->access_file, TRUE, r->connection->pool); if (svn_err) { ap_log_rerror(APLOG_MARK, 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. */ ((svn_err->apr_err >= APR_OS_START_USERERR && svn_err->apr_err < APR_OS_START_CANONERR) ? 0 : svn_err->apr_err), r, "Failed to load the AuthzSVNAccessFile: %s", svn_err_best_message(svn_err, errbuf, sizeof(errbuf))); svn_error_clear(svn_err); return DECLINED; } /* Cache the open repos for the next request on this connection */ apr_pool_userdata_set(access_conf, cache_key, NULL, r->connection->pool); } /* 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, r->user, authz_svn_type, &authz_access_granted, r->pool); if (svn_err) { ap_log_rerror(APLOG_MARK, 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. */ ((svn_err->apr_err >= APR_OS_START_USERERR && svn_err->apr_err < APR_OS_START_CANONERR) ? 0 : svn_err->apr_err), r, "Failed to perform access control: %s", svn_err_best_message(svn_err, errbuf, sizeof(errbuf))); svn_error_clear(svn_err); 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, r->user, svn_authz_write |svn_authz_recursive, &authz_access_granted, r->pool); if (svn_err) { ap_log_rerror(APLOG_MARK, 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. */ ((svn_err->apr_err >= APR_OS_START_USERERR && svn_err->apr_err < APR_OS_START_CANONERR) ? 0 : svn_err->apr_err), r, "Failed to perform access control: %s", svn_err_best_message(svn_err, errbuf, sizeof(errbuf))); svn_error_clear(svn_err); 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; } /* Log a message indicating the access control decision made about a * request. FILE and LINE should be supplied via the APLOG_MARK macro. * ALLOWED is boolean. REPOS_PATH and DEST_REPOS_PATH are information * about the request. DEST_REPOS_PATH may be NULL. */ static void log_access_verdict(const char *file, int line, const request_rec *r, int allowed, const char *repos_path, const char *dest_repos_path) { int level = allowed ? APLOG_INFO : APLOG_ERR; const char *verdict = allowed ? "granted" : "denied"; if (r->user) { if (dest_repos_path) { ap_log_rerror(file, line, level, 0, r, "Access %s: '%s' %s %s %s", verdict, r->user, r->method, repos_path, dest_repos_path); } else { ap_log_rerror(file, line, level, 0, r, "Access %s: '%s' %s %s", verdict, r->user, r->method, repos_path); } } else { if (dest_repos_path) { ap_log_rerror(file, line, level, 0, r, "Access %s: - %s %s %s", verdict, r->method, repos_path, dest_repos_path); } else { ap_log_rerror(file, line, level, 0, r, "Access %s: - %s %s", verdict, r->method, repos_path); } } } /* * 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; const char *dest_repos_path = NULL; int status; /* We are not configured to run */ if (!conf->anonymous || !conf->access_file) return DECLINED; if (ap_some_auth_required(r)) { /* 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. If anonymous * access is allowed, so is authenticated access, by definition * of the meaning of '*' 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; } } /* 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 (!ap_some_auth_required(r)) { log_access_verdict(APLOG_MARK, r, 0, repos_path, dest_repos_path); } return HTTP_FORBIDDEN; } if (status != OK) return status; log_access_verdict(APLOG_MARK, r, 1, 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; 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->access_file || !conf->no_auth_when_anon_ok || r->user) 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, 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; const char *dest_repos_path = NULL; int status; /* We are not configured to run */ if (!conf->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, 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, repos_path, dest_repos_path); return OK; } /* * Module flesh */ 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); } 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 */ };