Change the format of the revprops block sent in svnserve for
[svn.git] / subversion / libsvn_repos / authz.c
blob74d019a6206511ad507ea3e2a3d46704502d95bf
1 /* authz.c : path-based access control
3 * ====================================================================
4 * Copyright (c) 2000-2006 CollabNet. All rights reserved.
6 * This software is licensed as described in the file COPYING, which
7 * you should have received as part of this distribution. The terms
8 * are also available at http://subversion.tigris.org/license-1.html.
9 * If newer versions of this license are posted there, you may use a
10 * newer version instead, at your option.
12 * This software consists of voluntary contributions made by many
13 * individuals. For exact contribution history, see the revision
14 * history and logs, available at http://subversion.tigris.org/.
15 * ====================================================================
19 /*** Includes. ***/
21 #include <assert.h>
23 #include <apr_pools.h>
24 #include <apr_file_io.h>
26 #include "svn_pools.h"
27 #include "svn_error.h"
28 #include "svn_path.h"
29 #include "svn_repos.h"
30 #include "svn_config.h"
31 #include "svn_ctype.h"
34 /*** Structures. ***/
36 /* Information for the config enumerators called during authz
37 lookup. */
38 struct authz_lookup_baton {
39 /* The authz configuration. */
40 svn_config_t *config;
42 /* The user to authorize. */
43 const char *user;
45 /* Explicitly granted rights. */
46 svn_repos_authz_access_t allow;
47 /* Explicitly denied rights. */
48 svn_repos_authz_access_t deny;
50 /* The rights required by the caller of the lookup. */
51 svn_repos_authz_access_t required_access;
53 /* The following are used exclusively in recursive lookups. */
55 /* The path in the repository to authorize. */
56 const char *repos_path;
57 /* repos_path prefixed by the repository name. */
58 const char *qualified_repos_path;
60 /* Whether, at the end of a recursive lookup, access is granted. */
61 svn_boolean_t access;
64 /* Information for the config enumeration functions called during the
65 validation process. */
66 struct authz_validate_baton {
67 svn_config_t *config; /* The configuration file being validated. */
68 svn_error_t *err; /* The error being thrown out of the
69 enumerator, if any. */
72 /* Currently this structure is just a wrapper around a
73 svn_config_t. */
74 struct svn_authz_t
76 svn_config_t *cfg;
81 /*** Checking access. ***/
83 /* Determine whether the REQUIRED access is granted given what authz
84 * to ALLOW or DENY. Return TRUE if the REQUIRED access is
85 * granted.
87 * Access is granted either when no required access is explicitly
88 * denied (implicit grant), or when the required access is explicitly
89 * granted, overriding any denials.
91 static svn_boolean_t
92 authz_access_is_granted(svn_repos_authz_access_t allow,
93 svn_repos_authz_access_t deny,
94 svn_repos_authz_access_t required)
96 svn_repos_authz_access_t stripped_req =
97 required & (svn_authz_read | svn_authz_write);
99 if ((deny & required) == svn_authz_none)
100 return TRUE;
101 else if ((allow & required) == stripped_req)
102 return TRUE;
103 else
104 return FALSE;
108 /* Decide whether the REQUIRED access has been conclusively
109 * determined. Return TRUE if the given ALLOW/DENY authz are
110 * conclusive regarding the REQUIRED authz.
112 * Conclusive determination occurs when any of the REQUIRED authz are
113 * granted or denied by ALLOW/DENY.
115 static svn_boolean_t
116 authz_access_is_determined(svn_repos_authz_access_t allow,
117 svn_repos_authz_access_t deny,
118 svn_repos_authz_access_t required)
120 if ((deny & required) || (allow & required))
121 return TRUE;
122 else
123 return FALSE;
126 /* Return TRUE is USER equals ALIAS. The alias definitions are in the
127 "aliases" sections of CFG. Use POOL for temporary allocations during
128 the lookup. */
129 static svn_boolean_t
130 authz_alias_is_user(svn_config_t *cfg,
131 const char *alias,
132 const char *user,
133 apr_pool_t *pool)
135 const char *value;
137 svn_config_get(cfg, &value, "aliases", alias, NULL);
138 if (!value)
139 return FALSE;
141 if (strcmp(value, user) == 0)
142 return TRUE;
144 return FALSE;
148 /* Return TRUE if USER is in GROUP. The group definitions are in the
149 "groups" section of CFG. Use POOL for temporary allocations during
150 the lookup. */
151 static svn_boolean_t
152 authz_group_contains_user(svn_config_t *cfg,
153 const char *group,
154 const char *user,
155 apr_pool_t *pool)
157 const char *value;
158 apr_array_header_t *list;
159 int i;
161 svn_config_get(cfg, &value, "groups", group, NULL);
163 list = svn_cstring_split(value, ",", TRUE, pool);
165 for (i = 0; i < list->nelts; i++)
167 const char *group_user = APR_ARRAY_IDX(list, i, char *);
169 /* If the 'user' is a subgroup, recurse into it. */
170 if (*group_user == '@')
172 if (authz_group_contains_user(cfg, &group_user[1],
173 user, pool))
174 return TRUE;
177 /* If the 'user' is an alias, verify it. */
178 else if (*group_user == '&')
180 if (authz_alias_is_user(cfg, &group_user[1],
181 user, pool))
182 return TRUE;
185 /* If the user matches, stop. */
186 else if (strcmp(user, group_user) == 0)
187 return TRUE;
190 return FALSE;
194 /* Determines whether an authz rule applies to the current
195 * user, given the name part of the rule's name-value pair
196 * in RULE_MATCH_STRING and the authz_lookup_baton object
197 * B with the username in question.
199 static svn_boolean_t
200 authz_line_applies_to_user(const char *rule_match_string,
201 struct authz_lookup_baton *b,
202 apr_pool_t *pool)
204 /* If the rule has an inversion, recurse and invert the result. */
205 if (rule_match_string[0] == '~')
206 return !authz_line_applies_to_user(&rule_match_string[1], b, pool);
208 /* Check for special tokens. */
209 if (strcmp(rule_match_string, "$anonymous") == 0)
210 return (b->user == NULL);
211 if (strcmp(rule_match_string, "$authenticated") == 0)
212 return (b->user != NULL);
214 /* Check for a wildcard rule. */
215 if (strcmp(rule_match_string, "*") == 0)
216 return TRUE;
218 /* If we get here, then the rule is:
219 * - Not an inversion rule.
220 * - Not an authz token rule.
221 * - Not a wildcard rule.
223 * All that's left over is regular user or group specifications.
226 /* If the session is anonymous, then a user/group
227 * rule definitely won't match.
229 if (b->user == NULL)
230 return FALSE;
232 /* Process the rule depending on whether it is
233 * a user, alias or group rule.
235 if (rule_match_string[0] == '@')
236 return authz_group_contains_user(
237 b->config, &rule_match_string[1], b->user, pool);
238 else if (rule_match_string[0] == '&')
239 return authz_alias_is_user(
240 b->config, &rule_match_string[1], b->user, pool);
241 else
242 return (strcmp(b->user, rule_match_string) == 0);
246 /* Callback to parse one line of an authz file and update the
247 * authz_baton accordingly.
249 static svn_boolean_t
250 authz_parse_line(const char *name, const char *value,
251 void *baton, apr_pool_t *pool)
253 struct authz_lookup_baton *b = baton;
255 /* Stop if the rule doesn't apply to this user. */
256 if (!authz_line_applies_to_user(name, b, pool))
257 return TRUE;
259 /* Set the access grants for the rule. */
260 if (strchr(value, 'r'))
261 b->allow |= svn_authz_read;
262 else
263 b->deny |= svn_authz_read;
265 if (strchr(value, 'w'))
266 b->allow |= svn_authz_write;
267 else
268 b->deny |= svn_authz_write;
270 return TRUE;
274 /* Callback to parse a section and update the authz_baton if the
275 * section denies access to the subtree the baton describes.
277 static svn_boolean_t
278 authz_parse_section(const char *section_name, void *baton, apr_pool_t *pool)
280 struct authz_lookup_baton *b = baton;
281 svn_boolean_t conclusive;
283 /* Does the section apply to us? */
284 if (svn_path_is_ancestor(b->qualified_repos_path,
285 section_name) == FALSE
286 && svn_path_is_ancestor(b->repos_path,
287 section_name) == FALSE)
288 return TRUE;
290 /* Work out what this section grants. */
291 b->allow = b->deny = 0;
292 svn_config_enumerate2(b->config, section_name,
293 authz_parse_line, b, pool);
295 /* Has the section explicitly determined an access? */
296 conclusive = authz_access_is_determined(b->allow, b->deny,
297 b->required_access);
299 /* Is access granted OR inconclusive? */
300 b->access = authz_access_is_granted(b->allow, b->deny,
301 b->required_access)
302 || !conclusive;
304 /* As long as access isn't conclusively denied, carry on. */
305 return b->access;
309 /* Validate access to the given user for the given path. This
310 * function checks rules for exactly the given path, and first tries
311 * to access a section specific to the given repository before falling
312 * back to pan-repository rules.
314 * Update *access_granted to inform the caller of the outcome of the
315 * lookup. Return a boolean indicating whether the access rights were
316 * successfully determined.
318 static svn_boolean_t
319 authz_get_path_access(svn_config_t *cfg, const char *repos_name,
320 const char *path, const char *user,
321 svn_repos_authz_access_t required_access,
322 svn_boolean_t *access_granted,
323 apr_pool_t *pool)
325 const char *qualified_path;
326 struct authz_lookup_baton baton = { 0 };
328 baton.config = cfg;
329 baton.user = user;
331 /* Try to locate a repository-specific block first. */
332 qualified_path = apr_pstrcat(pool, repos_name, ":", path, NULL);
333 svn_config_enumerate2(cfg, qualified_path,
334 authz_parse_line, &baton, pool);
336 *access_granted = authz_access_is_granted(baton.allow, baton.deny,
337 required_access);
339 /* If the first test has determined access, stop now. */
340 if (authz_access_is_determined(baton.allow, baton.deny,
341 required_access))
342 return TRUE;
344 /* No repository specific rule, try pan-repository rules. */
345 svn_config_enumerate2(cfg, path, authz_parse_line, &baton, pool);
347 *access_granted = authz_access_is_granted(baton.allow, baton.deny,
348 required_access);
349 return authz_access_is_determined(baton.allow, baton.deny,
350 required_access);
354 /* Validate access to the given user for the subtree starting at the
355 * given path. This function walks the whole authz file in search of
356 * rules applying to paths in the requested subtree which deny the
357 * requested access.
359 * As soon as one is found, or else when the whole ACL file has been
360 * searched, return the updated authorization status.
362 static svn_boolean_t
363 authz_get_tree_access(svn_config_t *cfg, const char *repos_name,
364 const char *path, const char *user,
365 svn_repos_authz_access_t required_access,
366 apr_pool_t *pool)
368 struct authz_lookup_baton baton = { 0 };
370 baton.config = cfg;
371 baton.user = user;
372 baton.required_access = required_access;
373 baton.repos_path = path;
374 baton.qualified_repos_path = apr_pstrcat(pool, repos_name,
375 ":", path, NULL);
376 /* Default to access granted if no rules say otherwise. */
377 baton.access = TRUE;
379 svn_config_enumerate_sections2(cfg, authz_parse_section,
380 &baton, pool);
382 return baton.access;
386 /* Callback to parse sections of the configuration file, looking for
387 any kind of granted access. Implements the
388 svn_config_section_enumerator2_t interface. */
389 static svn_boolean_t
390 authz_global_parse_section(const char *section_name, void *baton,
391 apr_pool_t *pool)
393 struct authz_lookup_baton *b = baton;
395 /* Does the section apply to the query? */
396 if (section_name[0] == '/'
397 || strncmp(section_name, b->repos_path,
398 strlen(b->repos_path)) == 0)
400 b->allow = b->deny = svn_authz_none;
402 svn_config_enumerate2(b->config, section_name,
403 authz_parse_line, baton, pool);
404 b->access = authz_access_is_granted(b->allow, b->deny,
405 b->required_access);
407 /* Continue as long as we don't find a determined, granted access. */
408 return !(b->access
409 && authz_access_is_determined(b->allow, b->deny,
410 b->required_access));
413 return TRUE;
417 /* Walk through the authz CFG to check if USER has the REQUIRED_ACCESS
418 * to any path within the REPOSITORY. Return TRUE if so. Use POOL
419 * for temporary allocations. */
420 static svn_boolean_t
421 authz_get_global_access(svn_config_t *cfg, const char *repos_name,
422 const char *user,
423 svn_repos_authz_access_t required_access,
424 apr_pool_t *pool)
426 struct authz_lookup_baton baton = { 0 };
428 baton.config = cfg;
429 baton.user = user;
430 baton.required_access = required_access;
431 baton.access = FALSE; /* Deny access by default. */
432 baton.repos_path = apr_pstrcat(pool, repos_name, ":/", NULL);
434 svn_config_enumerate_sections2(cfg, authz_global_parse_section,
435 &baton, pool);
437 /* If walking the configuration was inconclusive, deny access. */
438 if (!authz_access_is_determined(baton.allow,
439 baton.deny, baton.required_access))
440 return FALSE;
442 return baton.access;
447 /*** Validating the authz file. ***/
449 /* Check for errors in GROUP's definition of CFG. The errors
450 * detected are references to non-existent groups and circular
451 * dependencies between groups. If an error is found, return
452 * SVN_ERR_AUTHZ_INVALID_CONFIG. Use POOL for temporary
453 * allocations only.
455 * CHECKED_GROUPS should be an empty (it is used for recursive calls).
457 static svn_error_t *
458 authz_group_walk(svn_config_t *cfg,
459 const char *group,
460 apr_hash_t *checked_groups,
461 apr_pool_t *pool)
463 const char *value;
464 apr_array_header_t *list;
465 int i;
467 svn_config_get(cfg, &value, "groups", group, NULL);
468 /* Having a non-existent group in the ACL configuration might be the
469 sign of a typo. Refuse to perform authz on uncertain rules. */
470 if (!value)
471 return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
472 "An authz rule refers to group '%s', "
473 "which is undefined",
474 group);
476 list = svn_cstring_split(value, ",", TRUE, pool);
478 for (i = 0; i < list->nelts; i++)
480 const char *group_user = APR_ARRAY_IDX(list, i, char *);
482 /* If the 'user' is a subgroup, recurse into it. */
483 if (*group_user == '@')
485 /* A circular dependency between groups is a Bad Thing. We
486 don't do authz with invalid ACL files. */
487 if (apr_hash_get(checked_groups, &group_user[1],
488 APR_HASH_KEY_STRING))
489 return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG,
490 NULL,
491 "Circular dependency between "
492 "groups '%s' and '%s'",
493 &group_user[1], group);
495 /* Add group to hash of checked groups. */
496 apr_hash_set(checked_groups, &group_user[1],
497 APR_HASH_KEY_STRING, "");
499 /* Recurse on that group. */
500 SVN_ERR(authz_group_walk(cfg, &group_user[1],
501 checked_groups, pool));
503 /* Remove group from hash of checked groups, so that we don't
504 incorrectly report an error if we see it again as part of
505 another group. */
506 apr_hash_set(checked_groups, &group_user[1],
507 APR_HASH_KEY_STRING, NULL);
509 else if (*group_user == '&')
511 const char *alias;
513 svn_config_get(cfg, &alias, "aliases", &group_user[1], NULL);
514 /* Having a non-existent alias in the ACL configuration might be the
515 sign of a typo. Refuse to perform authz on uncertain rules. */
516 if (!alias)
517 return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
518 "An authz rule refers to alias '%s', "
519 "which is undefined",
520 &group_user[1]);
524 return SVN_NO_ERROR;
528 /* Callback to perform some simple sanity checks on an authz rule.
530 * - If RULE_MATCH_STRING references a group or an alias, verify that
531 * the group or alias definition exists.
532 * - If RULE_MATCH_STRING specifies a token (starts with $), verify
533 * that the token name is valid.
534 * - If RULE_MATCH_STRING is using inversion, verify that it isn't
535 * doing it more than once within the one rule, and that it isn't
536 * "~*", as that would never match.
537 * - Check that VALUE part of the rule specifies only allowed rule
538 * flag characters ('r' and 'w').
540 * Return TRUE if the rule has no errors. Use BATON for context and
541 * error reporting.
543 static svn_boolean_t authz_validate_rule(const char *rule_match_string,
544 const char *value,
545 void *baton,
546 apr_pool_t *pool)
548 const char *val;
549 const char *match = rule_match_string;
550 struct authz_validate_baton *b = baton;
552 /* Make sure the user isn't using double-negatives. */
553 if (match[0] == '~')
555 /* Bump the pointer past the inversion for the other checks. */
556 match++;
558 /* Another inversion is a double negative; we can't not stop. */
559 if (match[0] == '~')
561 b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
562 "Rule '%s' has more than one "
563 "inversion; double negatives are "
564 "not permitted.",
565 rule_match_string);
566 return FALSE;
569 /* Make sure that the rule isn't "~*", which won't ever match. */
570 if (strcmp(match, "*") == 0)
572 b->err = svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
573 "Authz rules with match string '~*' "
574 "are not allowed, because they never "
575 "match anyone.");
576 return FALSE;
580 /* If the rule applies to a group, check its existence. */
581 if (match[0] == '@')
583 const char *group = &match[1];
585 svn_config_get(b->config, &val, "groups", group, NULL);
587 /* Having a non-existent group in the ACL configuration might be
588 the sign of a typo. Refuse to perform authz on uncertain
589 rules. */
590 if (!val)
592 b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
593 "An authz rule refers to group "
594 "'%s', which is undefined",
595 rule_match_string);
596 return FALSE;
600 /* If the rule applies to an alias, check its existence. */
601 if (match[0] == '&')
603 const char *alias = &match[1];
605 svn_config_get(b->config, &val, "aliases", alias, NULL);
607 if (!val)
609 b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
610 "An authz rule refers to alias "
611 "'%s', which is undefined",
612 rule_match_string);
613 return FALSE;
617 /* If the rule specifies a token, check its validity. */
618 if (match[0] == '$')
620 const char *token_name = &match[1];
622 if ((strcmp(token_name, "anonymous") != 0)
623 && (strcmp(token_name, "authenticated") != 0))
625 b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
626 "Unrecognized authz token '%s'.",
627 rule_match_string);
628 return FALSE;
632 val = value;
634 while (*val)
636 if (*val != 'r' && *val != 'w' && ! svn_ctype_isspace(*val))
638 b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
639 "The character '%c' in rule '%s' is not "
640 "allowed in authz rules", *val,
641 rule_match_string);
642 return FALSE;
645 ++val;
648 return TRUE;
651 /* Callback to check ALIAS's definition for validity. Use
652 BATON for context and error reporting. */
653 static svn_boolean_t authz_validate_alias(const char *alias,
654 const char *value,
655 void *baton,
656 apr_pool_t *pool)
658 /* No checking at the moment, every alias is valid */
659 return TRUE;
663 /* Callback to check GROUP's definition for cyclic dependancies. Use
664 BATON for context and error reporting. */
665 static svn_boolean_t authz_validate_group(const char *group,
666 const char *value,
667 void *baton,
668 apr_pool_t *pool)
670 struct authz_validate_baton *b = baton;
672 b->err = authz_group_walk(b->config, group, apr_hash_make(pool), pool);
673 if (b->err)
674 return FALSE;
676 return TRUE;
680 /* Callback to check the contents of the configuration section given
681 by NAME. Use BATON for context and error reporting. */
682 static svn_boolean_t authz_validate_section(const char *name,
683 void *baton,
684 apr_pool_t *pool)
686 struct authz_validate_baton *b = baton;
688 /* If the section is the groups definition, use the group checking
689 callback. Otherwise, use the rule checking callback. */
690 if (strncmp(name, "groups", 6) == 0)
691 svn_config_enumerate2(b->config, name, authz_validate_group,
692 baton, pool);
693 else if (strncmp(name, "aliases", 7) == 0)
694 svn_config_enumerate2(b->config, name, authz_validate_alias,
695 baton, pool);
696 else
697 svn_config_enumerate2(b->config, name, authz_validate_rule,
698 baton, pool);
700 if (b->err)
701 return FALSE;
703 return TRUE;
708 /*** Public functions. ***/
710 svn_error_t *
711 svn_repos_authz_read(svn_authz_t **authz_p, const char *file,
712 svn_boolean_t must_exist, apr_pool_t *pool)
714 svn_authz_t *authz = apr_palloc(pool, sizeof(*authz));
715 struct authz_validate_baton baton = { 0 };
717 baton.err = SVN_NO_ERROR;
719 /* Load the rule file. */
720 SVN_ERR(svn_config_read(&authz->cfg, file, must_exist, pool));
721 baton.config = authz->cfg;
723 /* Step through the entire rule file, stopping on error. */
724 svn_config_enumerate_sections2(authz->cfg, authz_validate_section,
725 &baton, pool);
726 SVN_ERR(baton.err);
728 *authz_p = authz;
729 return SVN_NO_ERROR;
733 svn_error_t *
734 svn_repos_authz_check_access(svn_authz_t *authz, const char *repos_name,
735 const char *path, const char *user,
736 svn_repos_authz_access_t required_access,
737 svn_boolean_t *access_granted,
738 apr_pool_t *pool)
740 const char *current_path = path;
742 /* If PATH is NULL, do a global access lookup. */
743 if (!path)
745 *access_granted = authz_get_global_access(authz->cfg, repos_name,
746 user, required_access,
747 pool);
748 return SVN_NO_ERROR;
751 /* Determine the granted access for the requested path. */
752 while (!authz_get_path_access(authz->cfg, repos_name,
753 current_path, user,
754 required_access,
755 access_granted,
756 pool))
758 /* Stop if the loop hits the repository root with no
759 results. */
760 if (current_path[0] == '/' && current_path[1] == '\0')
762 /* Deny access by default. */
763 *access_granted = FALSE;
764 return SVN_NO_ERROR;
767 /* Work back to the parent path. */
768 svn_path_split(current_path, &current_path, NULL, pool);
771 /* If the caller requested recursive access, we need to walk through
772 the entire authz config to see whether any child paths are denied
773 to the requested user. */
774 if (*access_granted && (required_access & svn_authz_recursive))
775 *access_granted = authz_get_tree_access(authz->cfg, repos_name, path,
776 user, required_access, pool);
778 return SVN_NO_ERROR;