1/* authz.c : path-based access control
2 *
3 * ====================================================================
4 *    Licensed to the Apache Software Foundation (ASF) under one
5 *    or more contributor license agreements.  See the NOTICE file
6 *    distributed with this work for additional information
7 *    regarding copyright ownership.  The ASF licenses this file
8 *    to you under the Apache License, Version 2.0 (the
9 *    "License"); you may not use this file except in compliance
10 *    with the License.  You may obtain a copy of the License at
11 *
12 *      http://www.apache.org/licenses/LICENSE-2.0
13 *
14 *    Unless required by applicable law or agreed to in writing,
15 *    software distributed under the License is distributed on an
16 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 *    KIND, either express or implied.  See the License for the
18 *    specific language governing permissions and limitations
19 *    under the License.
20 * ====================================================================
21 */
22
23
24/*** Includes. ***/
25
26#include <apr_pools.h>
27#include <apr_file_io.h>
28
29#include "svn_hash.h"
30#include "svn_pools.h"
31#include "svn_error.h"
32#include "svn_dirent_uri.h"
33#include "svn_path.h"
34#include "svn_repos.h"
35#include "svn_config.h"
36#include "svn_ctype.h"
37#include "private/svn_fspath.h"
38#include "repos.h"
39
40
41/*** Structures. ***/
42
43/* Information for the config enumerators called during authz
44   lookup. */
45struct authz_lookup_baton {
46  /* The authz configuration. */
47  svn_config_t *config;
48
49  /* The user to authorize. */
50  const char *user;
51
52  /* Explicitly granted rights. */
53  svn_repos_authz_access_t allow;
54  /* Explicitly denied rights. */
55  svn_repos_authz_access_t deny;
56
57  /* The rights required by the caller of the lookup. */
58  svn_repos_authz_access_t required_access;
59
60  /* The following are used exclusively in recursive lookups. */
61
62  /* The path in the repository (an fspath) to authorize. */
63  const char *repos_path;
64  /* repos_path prefixed by the repository name and a colon. */
65  const char *qualified_repos_path;
66
67  /* Whether, at the end of a recursive lookup, access is granted. */
68  svn_boolean_t access;
69};
70
71/* Information for the config enumeration functions called during the
72   validation process. */
73struct authz_validate_baton {
74  svn_config_t *config; /* The configuration file being validated. */
75  svn_error_t *err;     /* The error being thrown out of the
76                           enumerator, if any. */
77};
78
79/* Currently this structure is just a wrapper around a
80   svn_config_t. */
81struct svn_authz_t
82{
83  svn_config_t *cfg;
84};
85
86
87
88/*** Checking access. ***/
89
90/* Determine whether the REQUIRED access is granted given what authz
91 * to ALLOW or DENY.  Return TRUE if the REQUIRED access is
92 * granted.
93 *
94 * Access is granted either when no required access is explicitly
95 * denied (implicit grant), or when the required access is explicitly
96 * granted, overriding any denials.
97 */
98static svn_boolean_t
99authz_access_is_granted(svn_repos_authz_access_t allow,
100                        svn_repos_authz_access_t deny,
101                        svn_repos_authz_access_t required)
102{
103  svn_repos_authz_access_t stripped_req =
104    required & (svn_authz_read | svn_authz_write);
105
106  if ((deny & required) == svn_authz_none)
107    return TRUE;
108  else if ((allow & required) == stripped_req)
109    return TRUE;
110  else
111    return FALSE;
112}
113
114
115/* Decide whether the REQUIRED access has been conclusively
116 * determined.  Return TRUE if the given ALLOW/DENY authz are
117 * conclusive regarding the REQUIRED authz.
118 *
119 * Conclusive determination occurs when any of the REQUIRED authz are
120 * granted or denied by ALLOW/DENY.
121 */
122static svn_boolean_t
123authz_access_is_determined(svn_repos_authz_access_t allow,
124                           svn_repos_authz_access_t deny,
125                           svn_repos_authz_access_t required)
126{
127  if ((deny & required) || (allow & required))
128    return TRUE;
129  else
130    return FALSE;
131}
132
133/* Return TRUE is USER equals ALIAS. The alias definitions are in the
134   "aliases" sections of CFG. Use POOL for temporary allocations during
135   the lookup. */
136static svn_boolean_t
137authz_alias_is_user(svn_config_t *cfg,
138                    const char *alias,
139                    const char *user,
140                    apr_pool_t *pool)
141{
142  const char *value;
143
144  svn_config_get(cfg, &value, "aliases", alias, NULL);
145  if (!value)
146    return FALSE;
147
148  if (strcmp(value, user) == 0)
149    return TRUE;
150
151  return FALSE;
152}
153
154
155/* Return TRUE if USER is in GROUP.  The group definitions are in the
156   "groups" section of CFG.  Use POOL for temporary allocations during
157   the lookup. */
158static svn_boolean_t
159authz_group_contains_user(svn_config_t *cfg,
160                          const char *group,
161                          const char *user,
162                          apr_pool_t *pool)
163{
164  const char *value;
165  apr_array_header_t *list;
166  int i;
167
168  svn_config_get(cfg, &value, "groups", group, NULL);
169
170  list = svn_cstring_split(value, ",", TRUE, pool);
171
172  for (i = 0; i < list->nelts; i++)
173    {
174      const char *group_user = APR_ARRAY_IDX(list, i, char *);
175
176      /* If the 'user' is a subgroup, recurse into it. */
177      if (*group_user == '@')
178        {
179          if (authz_group_contains_user(cfg, &group_user[1],
180                                        user, pool))
181            return TRUE;
182        }
183
184      /* If the 'user' is an alias, verify it. */
185      else if (*group_user == '&')
186        {
187          if (authz_alias_is_user(cfg, &group_user[1],
188                                  user, pool))
189            return TRUE;
190        }
191
192      /* If the user matches, stop. */
193      else if (strcmp(user, group_user) == 0)
194        return TRUE;
195    }
196
197  return FALSE;
198}
199
200
201/* Determines whether an authz rule applies to the current
202 * user, given the name part of the rule's name-value pair
203 * in RULE_MATCH_STRING and the authz_lookup_baton object
204 * B with the username in question.
205 */
206static svn_boolean_t
207authz_line_applies_to_user(const char *rule_match_string,
208                           struct authz_lookup_baton *b,
209                           apr_pool_t *pool)
210{
211  /* If the rule has an inversion, recurse and invert the result. */
212  if (rule_match_string[0] == '~')
213    return !authz_line_applies_to_user(&rule_match_string[1], b, pool);
214
215  /* Check for special tokens. */
216  if (strcmp(rule_match_string, "$anonymous") == 0)
217    return (b->user == NULL);
218  if (strcmp(rule_match_string, "$authenticated") == 0)
219    return (b->user != NULL);
220
221  /* Check for a wildcard rule. */
222  if (strcmp(rule_match_string, "*") == 0)
223    return TRUE;
224
225  /* If we get here, then the rule is:
226   *  - Not an inversion rule.
227   *  - Not an authz token rule.
228   *  - Not a wildcard rule.
229   *
230   * All that's left over is regular user or group specifications.
231   */
232
233  /* If the session is anonymous, then a user/group
234   * rule definitely won't match.
235   */
236  if (b->user == NULL)
237    return FALSE;
238
239  /* Process the rule depending on whether it is
240   * a user, alias or group rule.
241   */
242  if (rule_match_string[0] == '@')
243    return authz_group_contains_user(
244      b->config, &rule_match_string[1], b->user, pool);
245  else if (rule_match_string[0] == '&')
246    return authz_alias_is_user(
247      b->config, &rule_match_string[1], b->user, pool);
248  else
249    return (strcmp(b->user, rule_match_string) == 0);
250}
251
252
253/* Callback to parse one line of an authz file and update the
254 * authz_baton accordingly.
255 */
256static svn_boolean_t
257authz_parse_line(const char *name, const char *value,
258                 void *baton, apr_pool_t *pool)
259{
260  struct authz_lookup_baton *b = baton;
261
262  /* Stop if the rule doesn't apply to this user. */
263  if (!authz_line_applies_to_user(name, b, pool))
264    return TRUE;
265
266  /* Set the access grants for the rule. */
267  if (strchr(value, 'r'))
268    b->allow |= svn_authz_read;
269  else
270    b->deny |= svn_authz_read;
271
272  if (strchr(value, 'w'))
273    b->allow |= svn_authz_write;
274  else
275    b->deny |= svn_authz_write;
276
277  return TRUE;
278}
279
280
281/* Return TRUE iff the access rules in SECTION_NAME apply to PATH_SPEC
282 * (which is a repository name, colon, and repository fspath, such as
283 * "myrepos:/trunk/foo").
284 */
285static svn_boolean_t
286is_applicable_section(const char *path_spec,
287                      const char *section_name)
288{
289  apr_size_t path_spec_len = strlen(path_spec);
290
291  return ((strncmp(path_spec, section_name, path_spec_len) == 0)
292          && (path_spec[path_spec_len - 1] == '/'
293              || section_name[path_spec_len] == '/'
294              || section_name[path_spec_len] == '\0'));
295}
296
297
298/* Callback to parse a section and update the authz_baton if the
299 * section denies access to the subtree the baton describes.
300 */
301static svn_boolean_t
302authz_parse_section(const char *section_name, void *baton, apr_pool_t *pool)
303{
304  struct authz_lookup_baton *b = baton;
305  svn_boolean_t conclusive;
306
307  /* Does the section apply to us? */
308  if (!is_applicable_section(b->qualified_repos_path, section_name)
309      && !is_applicable_section(b->repos_path, section_name))
310    return TRUE;
311
312  /* Work out what this section grants. */
313  b->allow = b->deny = 0;
314  svn_config_enumerate2(b->config, section_name,
315                        authz_parse_line, b, pool);
316
317  /* Has the section explicitly determined an access? */
318  conclusive = authz_access_is_determined(b->allow, b->deny,
319                                          b->required_access);
320
321  /* Is access granted OR inconclusive? */
322  b->access = authz_access_is_granted(b->allow, b->deny,
323                                      b->required_access)
324    || !conclusive;
325
326  /* As long as access isn't conclusively denied, carry on. */
327  return b->access;
328}
329
330
331/* Validate access to the given user for the given path.  This
332 * function checks rules for exactly the given path, and first tries
333 * to access a section specific to the given repository before falling
334 * back to pan-repository rules.
335 *
336 * Update *access_granted to inform the caller of the outcome of the
337 * lookup.  Return a boolean indicating whether the access rights were
338 * successfully determined.
339 */
340static svn_boolean_t
341authz_get_path_access(svn_config_t *cfg, const char *repos_name,
342                      const char *path, const char *user,
343                      svn_repos_authz_access_t required_access,
344                      svn_boolean_t *access_granted,
345                      apr_pool_t *pool)
346{
347  const char *qualified_path;
348  struct authz_lookup_baton baton = { 0 };
349
350  baton.config = cfg;
351  baton.user = user;
352
353  /* Try to locate a repository-specific block first. */
354  qualified_path = apr_pstrcat(pool, repos_name, ":", path, (char *)NULL);
355  svn_config_enumerate2(cfg, qualified_path,
356                        authz_parse_line, &baton, pool);
357
358  *access_granted = authz_access_is_granted(baton.allow, baton.deny,
359                                            required_access);
360
361  /* If the first test has determined access, stop now. */
362  if (authz_access_is_determined(baton.allow, baton.deny,
363                                 required_access))
364    return TRUE;
365
366  /* No repository specific rule, try pan-repository rules. */
367  svn_config_enumerate2(cfg, path, authz_parse_line, &baton, pool);
368
369  *access_granted = authz_access_is_granted(baton.allow, baton.deny,
370                                            required_access);
371  return authz_access_is_determined(baton.allow, baton.deny,
372                                    required_access);
373}
374
375
376/* Validate access to the given user for the subtree starting at the
377 * given path.  This function walks the whole authz file in search of
378 * rules applying to paths in the requested subtree which deny the
379 * requested access.
380 *
381 * As soon as one is found, or else when the whole ACL file has been
382 * searched, return the updated authorization status.
383 */
384static svn_boolean_t
385authz_get_tree_access(svn_config_t *cfg, const char *repos_name,
386                      const char *path, const char *user,
387                      svn_repos_authz_access_t required_access,
388                      apr_pool_t *pool)
389{
390  struct authz_lookup_baton baton = { 0 };
391
392  baton.config = cfg;
393  baton.user = user;
394  baton.required_access = required_access;
395  baton.repos_path = path;
396  baton.qualified_repos_path = apr_pstrcat(pool, repos_name,
397                                           ":", path, (char *)NULL);
398  /* Default to access granted if no rules say otherwise. */
399  baton.access = TRUE;
400
401  svn_config_enumerate_sections2(cfg, authz_parse_section,
402                                 &baton, pool);
403
404  return baton.access;
405}
406
407
408/* Callback to parse sections of the configuration file, looking for
409   any kind of granted access.  Implements the
410   svn_config_section_enumerator2_t interface. */
411static svn_boolean_t
412authz_get_any_access_parser_cb(const char *section_name, void *baton,
413                               apr_pool_t *pool)
414{
415  struct authz_lookup_baton *b = baton;
416
417  /* Does the section apply to the query? */
418  if (section_name[0] == '/'
419      || strncmp(section_name, b->qualified_repos_path,
420                 strlen(b->qualified_repos_path)) == 0)
421    {
422      b->allow = b->deny = svn_authz_none;
423
424      svn_config_enumerate2(b->config, section_name,
425                            authz_parse_line, baton, pool);
426      b->access = authz_access_is_granted(b->allow, b->deny,
427                                          b->required_access);
428
429      /* Continue as long as we don't find a determined, granted access. */
430      return !(b->access
431               && authz_access_is_determined(b->allow, b->deny,
432                                             b->required_access));
433    }
434
435  return TRUE;
436}
437
438
439/* Walk through the authz CFG to check if USER has the REQUIRED_ACCESS
440 * to any path within the REPOSITORY.  Return TRUE if so.  Use POOL
441 * for temporary allocations. */
442static svn_boolean_t
443authz_get_any_access(svn_config_t *cfg, const char *repos_name,
444                     const char *user,
445                     svn_repos_authz_access_t required_access,
446                     apr_pool_t *pool)
447{
448  struct authz_lookup_baton baton = { 0 };
449
450  baton.config = cfg;
451  baton.user = user;
452  baton.required_access = required_access;
453  baton.access = FALSE; /* Deny access by default. */
454  baton.repos_path = "/";
455  baton.qualified_repos_path = apr_pstrcat(pool, repos_name,
456                                           ":/", (char *)NULL);
457
458  /* We could have used svn_config_enumerate2 for "repos_name:/".
459   * However, this requires access for root explicitly (which the user
460   * may not always have). So we end up enumerating the sections in
461   * the authz CFG and stop on the first match with some access for
462   * this user. */
463  svn_config_enumerate_sections2(cfg, authz_get_any_access_parser_cb,
464                                 &baton, pool);
465
466  /* If walking the configuration was inconclusive, deny access. */
467  if (!authz_access_is_determined(baton.allow,
468                                  baton.deny, baton.required_access))
469    return FALSE;
470
471  return baton.access;
472}
473
474
475
476/*** Validating the authz file. ***/
477
478/* Check for errors in GROUP's definition of CFG.  The errors
479 * detected are references to non-existent groups and circular
480 * dependencies between groups.  If an error is found, return
481 * SVN_ERR_AUTHZ_INVALID_CONFIG.  Use POOL for temporary
482 * allocations only.
483 *
484 * CHECKED_GROUPS should be an empty (it is used for recursive calls).
485 */
486static svn_error_t *
487authz_group_walk(svn_config_t *cfg,
488                 const char *group,
489                 apr_hash_t *checked_groups,
490                 apr_pool_t *pool)
491{
492  const char *value;
493  apr_array_header_t *list;
494  int i;
495
496  svn_config_get(cfg, &value, "groups", group, NULL);
497  /* Having a non-existent group in the ACL configuration might be the
498     sign of a typo.  Refuse to perform authz on uncertain rules. */
499  if (!value)
500    return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
501                             "An authz rule refers to group '%s', "
502                             "which is undefined",
503                             group);
504
505  list = svn_cstring_split(value, ",", TRUE, pool);
506
507  for (i = 0; i < list->nelts; i++)
508    {
509      const char *group_user = APR_ARRAY_IDX(list, i, char *);
510
511      /* If the 'user' is a subgroup, recurse into it. */
512      if (*group_user == '@')
513        {
514          /* A circular dependency between groups is a Bad Thing.  We
515             don't do authz with invalid ACL files. */
516          if (svn_hash_gets(checked_groups, &group_user[1]))
517            return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG,
518                                     NULL,
519                                     "Circular dependency between "
520                                     "groups '%s' and '%s'",
521                                     &group_user[1], group);
522
523          /* Add group to hash of checked groups. */
524          svn_hash_sets(checked_groups, &group_user[1], "");
525
526          /* Recurse on that group. */
527          SVN_ERR(authz_group_walk(cfg, &group_user[1],
528                                   checked_groups, pool));
529
530          /* Remove group from hash of checked groups, so that we don't
531             incorrectly report an error if we see it again as part of
532             another group. */
533          svn_hash_sets(checked_groups, &group_user[1], NULL);
534        }
535      else if (*group_user == '&')
536        {
537          const char *alias;
538
539          svn_config_get(cfg, &alias, "aliases", &group_user[1], NULL);
540          /* Having a non-existent alias in the ACL configuration might be the
541             sign of a typo.  Refuse to perform authz on uncertain rules. */
542          if (!alias)
543            return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
544                                     "An authz rule refers to alias '%s', "
545                                     "which is undefined",
546                                     &group_user[1]);
547        }
548    }
549
550  return SVN_NO_ERROR;
551}
552
553
554/* Callback to perform some simple sanity checks on an authz rule.
555 *
556 * - If RULE_MATCH_STRING references a group or an alias, verify that
557 *   the group or alias definition exists.
558 * - If RULE_MATCH_STRING specifies a token (starts with $), verify
559 *   that the token name is valid.
560 * - If RULE_MATCH_STRING is using inversion, verify that it isn't
561 *   doing it more than once within the one rule, and that it isn't
562 *   "~*", as that would never match.
563 * - Check that VALUE part of the rule specifies only allowed rule
564 *   flag characters ('r' and 'w').
565 *
566 * Return TRUE if the rule has no errors. Use BATON for context and
567 * error reporting.
568 */
569static svn_boolean_t authz_validate_rule(const char *rule_match_string,
570                                         const char *value,
571                                         void *baton,
572                                         apr_pool_t *pool)
573{
574  const char *val;
575  const char *match = rule_match_string;
576  struct authz_validate_baton *b = baton;
577
578  /* Make sure the user isn't using double-negatives. */
579  if (match[0] == '~')
580    {
581      /* Bump the pointer past the inversion for the other checks. */
582      match++;
583
584      /* Another inversion is a double negative; we can't not stop. */
585      if (match[0] == '~')
586        {
587          b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
588                                     "Rule '%s' has more than one "
589                                     "inversion; double negatives are "
590                                     "not permitted.",
591                                     rule_match_string);
592          return FALSE;
593        }
594
595      /* Make sure that the rule isn't "~*", which won't ever match. */
596      if (strcmp(match, "*") == 0)
597        {
598          b->err = svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
599                                    "Authz rules with match string '~*' "
600                                    "are not allowed, because they never "
601                                    "match anyone.");
602          return FALSE;
603        }
604    }
605
606  /* If the rule applies to a group, check its existence. */
607  if (match[0] == '@')
608    {
609      const char *group = &match[1];
610
611      svn_config_get(b->config, &val, "groups", group, NULL);
612
613      /* Having a non-existent group in the ACL configuration might be
614         the sign of a typo.  Refuse to perform authz on uncertain
615         rules. */
616      if (!val)
617        {
618          b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
619                                     "An authz rule refers to group "
620                                     "'%s', which is undefined",
621                                     rule_match_string);
622          return FALSE;
623        }
624    }
625
626  /* If the rule applies to an alias, check its existence. */
627  if (match[0] == '&')
628    {
629      const char *alias = &match[1];
630
631      svn_config_get(b->config, &val, "aliases", alias, NULL);
632
633      if (!val)
634        {
635          b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
636                                     "An authz rule refers to alias "
637                                     "'%s', which is undefined",
638                                     rule_match_string);
639          return FALSE;
640        }
641     }
642
643  /* If the rule specifies a token, check its validity. */
644  if (match[0] == '$')
645    {
646      const char *token_name = &match[1];
647
648      if ((strcmp(token_name, "anonymous") != 0)
649       && (strcmp(token_name, "authenticated") != 0))
650        {
651          b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
652                                     "Unrecognized authz token '%s'.",
653                                     rule_match_string);
654          return FALSE;
655        }
656    }
657
658  val = value;
659
660  while (*val)
661    {
662      if (*val != 'r' && *val != 'w' && ! svn_ctype_isspace(*val))
663        {
664          b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
665                                     "The character '%c' in rule '%s' is not "
666                                     "allowed in authz rules", *val,
667                                     rule_match_string);
668          return FALSE;
669        }
670
671      ++val;
672    }
673
674  return TRUE;
675}
676
677/* Callback to check ALIAS's definition for validity.  Use
678   BATON for context and error reporting. */
679static svn_boolean_t authz_validate_alias(const char *alias,
680                                          const char *value,
681                                          void *baton,
682                                          apr_pool_t *pool)
683{
684  /* No checking at the moment, every alias is valid */
685  return TRUE;
686}
687
688
689/* Callback to check GROUP's definition for cyclic dependancies.  Use
690   BATON for context and error reporting. */
691static svn_boolean_t authz_validate_group(const char *group,
692                                          const char *value,
693                                          void *baton,
694                                          apr_pool_t *pool)
695{
696  struct authz_validate_baton *b = baton;
697
698  b->err = authz_group_walk(b->config, group, apr_hash_make(pool), pool);
699  if (b->err)
700    return FALSE;
701
702  return TRUE;
703}
704
705
706/* Callback to check the contents of the configuration section given
707   by NAME.  Use BATON for context and error reporting. */
708static svn_boolean_t authz_validate_section(const char *name,
709                                            void *baton,
710                                            apr_pool_t *pool)
711{
712  struct authz_validate_baton *b = baton;
713
714  /* Use the group checking callback for the "groups" section... */
715  if (strcmp(name, "groups") == 0)
716    svn_config_enumerate2(b->config, name, authz_validate_group,
717                          baton, pool);
718  /* ...and the alias checking callback for "aliases"... */
719  else if (strcmp(name, "aliases") == 0)
720    svn_config_enumerate2(b->config, name, authz_validate_alias,
721                          baton, pool);
722  /* ...but for everything else use the rule checking callback. */
723  else
724    {
725      /* Validate the section's name. Skip the optional REPOS_NAME. */
726      const char *fspath = strchr(name, ':');
727      if (fspath)
728        fspath++;
729      else
730        fspath = name;
731      if (! svn_fspath__is_canonical(fspath))
732        {
733          b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
734                                     "Section name '%s' contains non-canonical "
735                                     "fspath '%s'",
736                                     name, fspath);
737          return FALSE;
738        }
739
740      svn_config_enumerate2(b->config, name, authz_validate_rule,
741                            baton, pool);
742    }
743
744  if (b->err)
745    return FALSE;
746
747  return TRUE;
748}
749
750
751/* Walk the configuration in AUTHZ looking for any errors. */
752static svn_error_t *
753authz_validate(svn_authz_t *authz, apr_pool_t *pool)
754{
755  struct authz_validate_baton baton = { 0 };
756
757  baton.err = SVN_NO_ERROR;
758  baton.config = authz->cfg;
759
760  /* Step through the entire rule file stopping on error. */
761  svn_config_enumerate_sections2(authz->cfg, authz_validate_section,
762                                 &baton, pool);
763  SVN_ERR(baton.err);
764
765  return SVN_NO_ERROR;
766}
767
768
769/* Retrieve the file at DIRENT (contained in a repo) then parse it as a config
770 * file placing the result into CFG_P allocated in POOL.
771 *
772 * If DIRENT cannot be parsed as a config file then an error is returned.  The
773 * contents of CFG_P is then undefined.  If MUST_EXIST is TRUE, a missing
774 * authz file is also an error.
775 *
776 * SCRATCH_POOL will be used for temporary allocations. */
777static svn_error_t *
778authz_retrieve_config_repo(svn_config_t **cfg_p, const char *dirent,
779                          svn_boolean_t must_exist,
780                          apr_pool_t *result_pool, apr_pool_t *scratch_pool)
781{
782  svn_error_t *err;
783  svn_repos_t *repos;
784  const char *repos_root_dirent;
785  const char *fs_path;
786  svn_fs_t *fs;
787  svn_fs_root_t *root;
788  svn_revnum_t youngest_rev;
789  svn_node_kind_t node_kind;
790  svn_stream_t *contents;
791
792  /* Search for a repository in the full path. */
793  repos_root_dirent = svn_repos_find_root_path(dirent, scratch_pool);
794  if (!repos_root_dirent)
795    return svn_error_createf(SVN_ERR_RA_LOCAL_REPOS_NOT_FOUND, NULL,
796                             "Unable to find repository at '%s'", dirent);
797
798  /* Attempt to open a repository at repos_root_dirent. */
799  SVN_ERR(svn_repos_open2(&repos, repos_root_dirent, NULL, scratch_pool));
800
801  fs_path = &dirent[strlen(repos_root_dirent)];
802
803  /* Root path is always a directory so no reason to go any further */
804  if (*fs_path == '\0')
805    return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
806                             "'/' is not a file in repo '%s'",
807                             repos_root_dirent);
808
809  /* We skip some things that are non-important for how we're going to use
810   * this repo connection.  We do not set any capabilities since none of
811   * the current ones are important for what we're doing.  We also do not
812   * setup the environment that repos hooks would run under since we won't
813   * be triggering any. */
814
815  /* Get the filesystem. */
816  fs = svn_repos_fs(repos);
817
818  /* Find HEAD and the revision root */
819  SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, scratch_pool));
820  SVN_ERR(svn_fs_revision_root(&root, fs, youngest_rev, scratch_pool));
821
822  SVN_ERR(svn_fs_check_path(&node_kind, root, fs_path, scratch_pool));
823  if (node_kind == svn_node_none)
824    {
825      if (!must_exist)
826        {
827          SVN_ERR(svn_config_create2(cfg_p, TRUE, TRUE, result_pool));
828          return SVN_NO_ERROR;
829        }
830      else
831        {
832          return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
833                                   "'%s' path not found in repo '%s'", fs_path,
834                                   repos_root_dirent);
835        }
836    }
837  else if (node_kind != svn_node_file)
838    {
839      return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
840                               "'%s' is not a file in repo '%s'", fs_path,
841                               repos_root_dirent);
842    }
843
844  SVN_ERR(svn_fs_file_contents(&contents, root, fs_path, scratch_pool));
845  err = svn_config_parse(cfg_p, contents, TRUE, TRUE, result_pool);
846
847  /* Add the URL to the error stack since the parser doesn't have it. */
848  if (err != SVN_NO_ERROR)
849    return svn_error_createf(err->apr_err, err,
850                             "Error while parsing config file: '%s' in repo '%s':",
851                             fs_path, repos_root_dirent);
852
853  return SVN_NO_ERROR;
854}
855
856/* Given a PATH which might be a relative repo URL (^/), an absolute
857 * local repo URL (file://), an absolute path outside of the repo
858 * or a location in the Windows registry.
859 *
860 * Retrieve the configuration data that PATH points at and parse it into
861 * CFG_P allocated in POOL.
862 *
863 * If PATH cannot be parsed as a config file then an error is returned.  The
864 * contents of CFG_P is then undefined.  If MUST_EXIST is TRUE, a missing
865 * authz file is also an error.
866 *
867 * REPOS_ROOT points at the root of the repos you are
868 * going to apply the authz against, can be NULL if you are sure that you
869 * don't have a repos relative URL in PATH. */
870static svn_error_t *
871authz_retrieve_config(svn_config_t **cfg_p, const char *path,
872                      svn_boolean_t must_exist, apr_pool_t *pool)
873{
874  if (svn_path_is_url(path))
875    {
876      const char *dirent;
877      svn_error_t *err;
878      apr_pool_t *scratch_pool = svn_pool_create(pool);
879
880      err = svn_uri_get_dirent_from_file_url(&dirent, path, scratch_pool);
881
882      if (err == SVN_NO_ERROR)
883        err = authz_retrieve_config_repo(cfg_p, dirent, must_exist, pool,
884                                         scratch_pool);
885
886      /* Close the repos and streams we opened. */
887      svn_pool_destroy(scratch_pool);
888
889      return err;
890    }
891  else
892    {
893      /* Outside of repo file or Windows registry*/
894      SVN_ERR(svn_config_read3(cfg_p, path, must_exist, TRUE, TRUE, pool));
895    }
896
897  return SVN_NO_ERROR;
898}
899
900
901/* Callback to copy (name, value) group into the "groups" section
902   of another configuration. */
903static svn_boolean_t
904authz_copy_group(const char *name, const char *value,
905                 void *baton, apr_pool_t *pool)
906{
907  svn_config_t *authz_cfg = baton;
908
909  svn_config_set(authz_cfg, SVN_CONFIG_SECTION_GROUPS, name, value);
910
911  return TRUE;
912}
913
914/* Copy group definitions from GROUPS_CFG to the resulting AUTHZ.
915 * If AUTHZ already contains any group definition, report an error.
916 * Use POOL for temporary allocations. */
917static svn_error_t *
918authz_copy_groups(svn_authz_t *authz, svn_config_t *groups_cfg,
919                  apr_pool_t *pool)
920{
921  /* Easy out: we prohibit local groups in the authz file when global
922     groups are being used. */
923  if (svn_config_has_section(authz->cfg, SVN_CONFIG_SECTION_GROUPS))
924    {
925      return svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
926                              "Authz file cannot contain any groups "
927                              "when global groups are being used.");
928    }
929
930  svn_config_enumerate2(groups_cfg, SVN_CONFIG_SECTION_GROUPS,
931                        authz_copy_group, authz->cfg, pool);
932
933  return SVN_NO_ERROR;
934}
935
936svn_error_t *
937svn_repos__authz_read(svn_authz_t **authz_p, const char *path,
938                      const char *groups_path, svn_boolean_t must_exist,
939                      svn_boolean_t accept_urls, apr_pool_t *pool)
940{
941  svn_authz_t *authz = apr_palloc(pool, sizeof(*authz));
942
943  /* Load the authz file */
944  if (accept_urls)
945    SVN_ERR(authz_retrieve_config(&authz->cfg, path, must_exist, pool));
946  else
947    SVN_ERR(svn_config_read3(&authz->cfg, path, must_exist, TRUE, TRUE, pool));
948
949  if (groups_path)
950    {
951      svn_config_t *groups_cfg;
952      svn_error_t *err;
953
954      /* Load the groups file */
955      if (accept_urls)
956        SVN_ERR(authz_retrieve_config(&groups_cfg, groups_path, must_exist,
957                                      pool));
958      else
959        SVN_ERR(svn_config_read3(&groups_cfg, groups_path, must_exist,
960                                 TRUE, TRUE, pool));
961
962      /* Copy the groups from groups_cfg into authz. */
963      err = authz_copy_groups(authz, groups_cfg, pool);
964
965      /* Add the paths to the error stack since the authz_copy_groups
966         routine knows nothing about them. */
967      if (err != SVN_NO_ERROR)
968        return svn_error_createf(err->apr_err, err,
969                                 "Error reading authz file '%s' with "
970                                 "groups file '%s':", path, groups_path);
971    }
972
973  /* Make sure there are no errors in the configuration. */
974  SVN_ERR(authz_validate(authz, pool));
975
976  *authz_p = authz;
977  return SVN_NO_ERROR;
978}
979
980
981
982/*** Public functions. ***/
983
984svn_error_t *
985svn_repos_authz_read2(svn_authz_t **authz_p, const char *path,
986                      const char *groups_path, svn_boolean_t must_exist,
987                      apr_pool_t *pool)
988{
989  return svn_repos__authz_read(authz_p, path, groups_path, must_exist,
990                               TRUE, pool);
991}
992
993
994svn_error_t *
995svn_repos_authz_parse(svn_authz_t **authz_p, svn_stream_t *stream,
996                      svn_stream_t *groups_stream, apr_pool_t *pool)
997{
998  svn_authz_t *authz = apr_palloc(pool, sizeof(*authz));
999
1000  /* Parse the authz stream */
1001  SVN_ERR(svn_config_parse(&authz->cfg, stream, TRUE, TRUE, pool));
1002
1003  if (groups_stream)
1004    {
1005      svn_config_t *groups_cfg;
1006
1007      /* Parse the groups stream */
1008      SVN_ERR(svn_config_parse(&groups_cfg, groups_stream, TRUE, TRUE, pool));
1009
1010      SVN_ERR(authz_copy_groups(authz, groups_cfg, pool));
1011    }
1012
1013  /* Make sure there are no errors in the configuration. */
1014  SVN_ERR(authz_validate(authz, pool));
1015
1016  *authz_p = authz;
1017  return SVN_NO_ERROR;
1018}
1019
1020
1021svn_error_t *
1022svn_repos_authz_check_access(svn_authz_t *authz, const char *repos_name,
1023                             const char *path, const char *user,
1024                             svn_repos_authz_access_t required_access,
1025                             svn_boolean_t *access_granted,
1026                             apr_pool_t *pool)
1027{
1028  const char *current_path;
1029
1030  if (!repos_name)
1031    repos_name = "";
1032
1033  /* If PATH is NULL, check if the user has *any* access. */
1034  if (!path)
1035    {
1036      *access_granted = authz_get_any_access(authz->cfg, repos_name,
1037                                             user, required_access, pool);
1038      return SVN_NO_ERROR;
1039    }
1040
1041  /* Sanity check. */
1042  SVN_ERR_ASSERT(path[0] == '/');
1043
1044  /* Determine the granted access for the requested path. */
1045  path = svn_fspath__canonicalize(path, pool);
1046  current_path = path;
1047
1048  while (!authz_get_path_access(authz->cfg, repos_name,
1049                                current_path, user,
1050                                required_access,
1051                                access_granted,
1052                                pool))
1053    {
1054      /* Stop if the loop hits the repository root with no
1055         results. */
1056      if (current_path[0] == '/' && current_path[1] == '\0')
1057        {
1058          /* Deny access by default. */
1059          *access_granted = FALSE;
1060          return SVN_NO_ERROR;
1061        }
1062
1063      /* Work back to the parent path. */
1064      current_path = svn_fspath__dirname(current_path, pool);
1065    }
1066
1067  /* If the caller requested recursive access, we need to walk through
1068     the entire authz config to see whether any child paths are denied
1069     to the requested user. */
1070  if (*access_granted && (required_access & svn_authz_recursive))
1071    *access_granted = authz_get_tree_access(authz->cfg, repos_name, path,
1072                                            user, required_access, pool);
1073
1074  return SVN_NO_ERROR;
1075}
1076