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 * ====================================================================
23 #include <apr_pools.h>
24 #include <apr_file_io.h>
26 #include "svn_pools.h"
27 #include "svn_error.h"
29 #include "svn_repos.h"
30 #include "svn_config.h"
31 #include "svn_ctype.h"
36 /* Information for the config enumerators called during authz
38 struct authz_lookup_baton
{
39 /* The authz configuration. */
42 /* The user to authorize. */
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. */
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
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
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.
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
)
101 else if ((allow
& required
) == stripped_req
)
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.
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
))
126 /* Return TRUE is USER equals ALIAS. The alias definitions are in the
127 "aliases" sections of CFG. Use POOL for temporary allocations during
130 authz_alias_is_user(svn_config_t
*cfg
,
137 svn_config_get(cfg
, &value
, "aliases", alias
, NULL
);
141 if (strcmp(value
, user
) == 0)
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
152 authz_group_contains_user(svn_config_t
*cfg
,
158 apr_array_header_t
*list
;
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],
177 /* If the 'user' is an alias, verify it. */
178 else if (*group_user
== '&')
180 if (authz_alias_is_user(cfg
, &group_user
[1],
185 /* If the user matches, stop. */
186 else if (strcmp(user
, group_user
) == 0)
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.
200 authz_line_applies_to_user(const char *rule_match_string
,
201 struct authz_lookup_baton
*b
,
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)
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.
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
);
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.
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
))
259 /* Set the access grants for the rule. */
260 if (strchr(value
, 'r'))
261 b
->allow
|= svn_authz_read
;
263 b
->deny
|= svn_authz_read
;
265 if (strchr(value
, 'w'))
266 b
->allow
|= svn_authz_write
;
268 b
->deny
|= svn_authz_write
;
274 /* Callback to parse a section and update the authz_baton if the
275 * section denies access to the subtree the baton describes.
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
)
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
,
299 /* Is access granted OR inconclusive? */
300 b
->access
= authz_access_is_granted(b
->allow
, b
->deny
,
304 /* As long as access isn't conclusively denied, carry on. */
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.
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
,
325 const char *qualified_path
;
326 struct authz_lookup_baton baton
= { 0 };
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
,
339 /* If the first test has determined access, stop now. */
340 if (authz_access_is_determined(baton
.allow
, baton
.deny
,
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
,
349 return authz_access_is_determined(baton
.allow
, baton
.deny
,
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
359 * As soon as one is found, or else when the whole ACL file has been
360 * searched, return the updated authorization status.
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
,
368 struct authz_lookup_baton baton
= { 0 };
372 baton
.required_access
= required_access
;
373 baton
.repos_path
= path
;
374 baton
.qualified_repos_path
= apr_pstrcat(pool
, repos_name
,
376 /* Default to access granted if no rules say otherwise. */
379 svn_config_enumerate_sections2(cfg
, authz_parse_section
,
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. */
390 authz_global_parse_section(const char *section_name
, void *baton
,
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
,
407 /* Continue as long as we don't find a determined, granted access. */
409 && authz_access_is_determined(b
->allow
, b
->deny
,
410 b
->required_access
));
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. */
421 authz_get_global_access(svn_config_t
*cfg
, const char *repos_name
,
423 svn_repos_authz_access_t required_access
,
426 struct authz_lookup_baton baton
= { 0 };
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
,
437 /* If walking the configuration was inconclusive, deny access. */
438 if (!authz_access_is_determined(baton
.allow
,
439 baton
.deny
, baton
.required_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
455 * CHECKED_GROUPS should be an empty (it is used for recursive calls).
458 authz_group_walk(svn_config_t
*cfg
,
460 apr_hash_t
*checked_groups
,
464 apr_array_header_t
*list
;
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. */
471 return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG
, NULL
,
472 "An authz rule refers to group '%s', "
473 "which is undefined",
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
,
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
506 apr_hash_set(checked_groups
, &group_user
[1],
507 APR_HASH_KEY_STRING
, NULL
);
509 else if (*group_user
== '&')
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. */
517 return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG
, NULL
,
518 "An authz rule refers to alias '%s', "
519 "which is undefined",
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
543 static svn_boolean_t
authz_validate_rule(const char *rule_match_string
,
549 const char *match
= rule_match_string
;
550 struct authz_validate_baton
*b
= baton
;
552 /* Make sure the user isn't using double-negatives. */
555 /* Bump the pointer past the inversion for the other checks. */
558 /* Another inversion is a double negative; we can't not stop. */
561 b
->err
= svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG
, NULL
,
562 "Rule '%s' has more than one "
563 "inversion; double negatives are "
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 "
580 /* If the rule applies to a group, check its existence. */
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
592 b
->err
= svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG
, NULL
,
593 "An authz rule refers to group "
594 "'%s', which is undefined",
600 /* If the rule applies to an alias, check its existence. */
603 const char *alias
= &match
[1];
605 svn_config_get(b
->config
, &val
, "aliases", alias
, NULL
);
609 b
->err
= svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG
, NULL
,
610 "An authz rule refers to alias "
611 "'%s', which is undefined",
617 /* If the rule specifies a token, check its validity. */
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'.",
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
,
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
,
658 /* No checking at the moment, every alias is valid */
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
,
670 struct authz_validate_baton
*b
= baton
;
672 b
->err
= authz_group_walk(b
->config
, group
, apr_hash_make(pool
), pool
);
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
,
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
,
693 else if (strncmp(name
, "aliases", 7) == 0)
694 svn_config_enumerate2(b
->config
, name
, authz_validate_alias
,
697 svn_config_enumerate2(b
->config
, name
, authz_validate_rule
,
708 /*** Public functions. ***/
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
,
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
,
740 const char *current_path
= path
;
742 /* If PATH is NULL, do a global access lookup. */
745 *access_granted
= authz_get_global_access(authz
->cfg
, repos_name
,
746 user
, required_access
,
751 /* Determine the granted access for the requested path. */
752 while (!authz_get_path_access(authz
->cfg
, repos_name
,
758 /* Stop if the loop hits the repository root with no
760 if (current_path
[0] == '/' && current_path
[1] == '\0')
762 /* Deny access by default. */
763 *access_granted
= FALSE
;
767 /* Work back to the parent path. */
768 svn_path_split(current_path
, ¤t_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
);