diff options
Diffstat (limited to 'subversion/libsvn_repos/authz_parse.c')
-rw-r--r-- | subversion/libsvn_repos/authz_parse.c | 1442 |
1 files changed, 1442 insertions, 0 deletions
diff --git a/subversion/libsvn_repos/authz_parse.c b/subversion/libsvn_repos/authz_parse.c new file mode 100644 index 000000000000..23612dedd490 --- /dev/null +++ b/subversion/libsvn_repos/authz_parse.c @@ -0,0 +1,1442 @@ +/* authz_parse.c : Parser for path-based access control + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ + +#include <apr_fnmatch.h> +#include <apr_tables.h> + +#include "svn_ctype.h" +#include "svn_error.h" +#include "svn_hash.h" +#include "svn_iter.h" +#include "svn_pools.h" +#include "svn_repos.h" + +#include "private/svn_fspath.h" +#include "private/svn_config_private.h" +#include "private/svn_sorts_private.h" +#include "private/svn_string_private.h" +#include "private/svn_subr_private.h" + +#include "svn_private_config.h" + +#include "authz.h" + + +/* Temporary ACL constructed by the parser. */ +typedef struct parsed_acl_t +{ + /* The global ACL. + The strings in ACL.rule are allocated from the result pool. + ACL.user_access is null during the parsing stage. */ + authz_acl_t acl; + + /* The set of access control entries. In the second pass, aliases in + these entries will be expanded and equivalent entries will be + merged. The entries are allocated from the parser pool. */ + apr_hash_t *aces; + + /* The set of access control entries that use aliases. In the second + pass, aliases in these entries will be expanded and merged into ACES. + The entries are allocated from the parser pool. */ + apr_hash_t *alias_aces; +} parsed_acl_t; + + +/* Temporary group definition constructed by the authz/group parser. + Once all groups and aliases are defined, a second pass over these + data will recursively expand group memberships. */ +typedef struct parsed_group_t +{ + svn_boolean_t local_group; + apr_array_header_t *members; +} parsed_group_t; + + +/* Baton for the parser constructor. */ +typedef struct ctor_baton_t +{ + /* The final output of the parser. */ + authz_full_t *authz; + + /* Interned-string set, allocated in AUTHZ->pool. + Stores singleton instances of user, group and repository names, + which are used by members of the AUTHZ structure. By reusing the + same immutable string multiple times, we reduce the size of the + authz representation in the result pool. + + N.B.: Whilst the strings are allocated from teh result pool, the + hash table itself is not. */ + apr_hash_t *strings; + + /* A set of all the sections that were seen in the authz or global + groups file. Rules, aliases and groups may each only be defined + once in the authz file. The global groups file may only contain a + [groups] section. */ + apr_hash_t *sections; + + /* The name of the section we're currently parsing. */ + const char *section; + + /* TRUE iff we're parsing the global groups file. */ + svn_boolean_t parsing_groups; + + /* TRUE iff we're parsing a [groups] section. */ + svn_boolean_t in_groups; + + /* TRUE iff we're parsing an [aliases] section. */ + svn_boolean_t in_aliases; + + /* A set of all the unique rules we parsed from the section names. */ + apr_hash_t *parsed_rules; + + /* Temporary parsed-groups definitions. */ + apr_hash_t *parsed_groups; + + /* Temporary alias mappings. */ + apr_hash_t *parsed_aliases; + + /* Temporary parsed-acl definitions. */ + apr_array_header_t *parsed_acls; + + /* Temporary expanded groups definitions. */ + apr_hash_t *expanded_groups; + + /* The temporary ACL we're currently constructing. */ + parsed_acl_t *current_acl; + + /* Temporary buffers used to parse a rule into segments. */ + svn_membuf_t rule_path_buffer; + svn_stringbuf_t *rule_string_buffer; + + /* The parser's scratch pool. This may not be the same pool as + passed to the constructor callbacks, that is supposed to be an + iteration pool maintained by the generic parser. + + N.B.: The result pool is AUTHZ->pool. */ + apr_pool_t *parser_pool; +} ctor_baton_t; + + +/* An empty string with a known address. */ +static const char interned_empty_string[] = ""; + +/* The name of the aliases section. */ +static const char aliases_section[] = "aliases"; + +/* The name of the groups section. */ +static const char groups_section[] = "groups"; + +/* The token indicating that an authz rule contains wildcards. */ +static const char glob_rule_token[] = "glob"; + +/* The anonymous access token. */ +static const char anon_access_token[] = "$anonymous"; + +/* The authenticated access token. */ +static const char authn_access_token[] = "$authenticated"; + + +/* Initialize a rights structure. + The minimum rights start with all available access and are later + bitwise-and'ed with actual access rights. The maximum rights begin + empty and are later bitwise-and'ed with actual rights. */ +static void init_rights(authz_rights_t *rights) +{ + rights->min_access = authz_access_write; + rights->max_access = authz_access_none; + } + +/* Initialize a global rights structure. + The USER string must be interned or statically initialized. */ +static void +init_global_rights(authz_global_rights_t *gr, const char *user, + apr_pool_t *result_pool) +{ + gr->user = user; + init_rights(&gr->all_repos_rights); + init_rights(&gr->any_repos_rights); + gr->per_repos_rights = apr_hash_make(result_pool); +} + + +/* Insert the default global ACL into the parsed ACLs. */ +static void +insert_default_acl(ctor_baton_t *cb) +{ + parsed_acl_t *acl = &APR_ARRAY_PUSH(cb->parsed_acls, parsed_acl_t); + acl->acl.sequence_number = 0; + acl->acl.rule.repos = interned_empty_string; + acl->acl.rule.len = 0; + acl->acl.rule.path = NULL; + acl->acl.anon_access = authz_access_none; + acl->acl.has_anon_access = TRUE; + acl->acl.authn_access = authz_access_none; + acl->acl.has_authn_access = TRUE; + acl->acl.user_access = NULL; + acl->aces = svn_hash__make(cb->parser_pool); + acl->alias_aces = svn_hash__make(cb->parser_pool); +} + + +/* Initialize a constuctor baton. */ +static ctor_baton_t * +create_ctor_baton(apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_pool_t *const parser_pool = svn_pool_create(scratch_pool); + ctor_baton_t *const cb = apr_pcalloc(parser_pool, sizeof(*cb)); + + authz_full_t *const authz = apr_pcalloc(result_pool, sizeof(*authz)); + init_global_rights(&authz->anon_rights, anon_access_token, result_pool); + init_global_rights(&authz->authn_rights, authn_access_token, result_pool); + authz->user_rights = svn_hash__make(result_pool); + authz->pool = result_pool; + + cb->authz = authz; + cb->strings = svn_hash__make(parser_pool); + + cb->sections = svn_hash__make(parser_pool); + cb->section = NULL; + cb->parsing_groups = FALSE; + cb->in_groups = FALSE; + cb->in_aliases = FALSE; + + cb->parsed_rules = svn_hash__make(parser_pool); + cb->parsed_groups = svn_hash__make(parser_pool); + cb->parsed_aliases = svn_hash__make(parser_pool); + cb->parsed_acls = apr_array_make(parser_pool, 64, sizeof(parsed_acl_t)); + cb->current_acl = NULL; + + svn_membuf__create(&cb->rule_path_buffer, 0, parser_pool); + cb->rule_string_buffer = svn_stringbuf_create_empty(parser_pool); + + cb->parser_pool = parser_pool; + + insert_default_acl(cb); + + return cb; +} + + +/* Create and store per-user global rights. + The USER string must be interned or statically initialized. */ +static void +prepare_global_rights(ctor_baton_t *cb, const char *user) +{ + authz_global_rights_t *gr = svn_hash_gets(cb->authz->user_rights, user); + if (!gr) + { + gr = apr_palloc(cb->authz->pool, sizeof(*gr)); + init_global_rights(gr, user, cb->authz->pool); + svn_hash_sets(cb->authz->user_rights, gr->user, gr); + } +} + + +/* Internalize a string that will be referenced by the parsed svn_authz_t. + If LEN is (apr_size_t)-1, assume the string is NUL-terminated. */ +static const char * +intern_string(ctor_baton_t *cb, const char *str, apr_size_t len) +{ + const char *interned; + + if (len == (apr_size_t)-1) + len = strlen(str); + + interned = apr_hash_get(cb->strings, str, len); + if (!interned) + { + interned = apr_pstrmemdup(cb->authz->pool, str, len); + apr_hash_set(cb->strings, interned, len, interned); + } + return interned; +} + + +/* Helper for rules_open_section and groups_open_section. */ +static svn_error_t * +check_open_section(ctor_baton_t *cb, svn_stringbuf_t *section) +{ + SVN_ERR_ASSERT(!cb->current_acl && !cb->section); + if (apr_hash_get(cb->sections, section->data, section->len)) + { + if (cb->parsing_groups) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Section appears more than once" + " in the global groups file: [%s]"), + section->data); + else + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Section appears more than once" + " in the authz file: [%s]"), + section->data); + } + + cb->section = apr_pstrmemdup(cb->parser_pool, section->data, section->len); + svn_hash_sets(cb->sections, cb->section, interned_empty_string); + return SVN_NO_ERROR; +} + + +/* Constructor callback: Begins the [groups] section. */ +static svn_error_t * +groups_open_section(void *baton, svn_stringbuf_t *section) +{ + ctor_baton_t *const cb = baton; + + if (cb->parsing_groups) + SVN_ERR(check_open_section(cb, section)); + + if (0 == strcmp(section->data, groups_section)) + { + cb->in_groups = TRUE; + return SVN_NO_ERROR; + } + + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + (cb->parsing_groups + ? _("Section is not valid in the global group file: [%s]") + : _("Section is not valid in the authz file: [%s]")), + section->data); +} + + +/* Constructor callback: Parses a group declaration. */ +static svn_error_t * +groups_add_value(void *baton, svn_stringbuf_t *section, + svn_stringbuf_t *option, svn_stringbuf_t *value) +{ + ctor_baton_t *const cb = baton; + const char *group; + apr_size_t group_len; + + SVN_ERR_ASSERT(cb->in_groups); + + if (strchr("@$&*~", *option->data)) + { + if (cb->parsing_groups) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Global group name '%s' may not begin with '%c'"), + option->data, *option->data); + else + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Group name '%s' may not begin with '%c'"), + option->data, *option->data); + } + + /* Decorate the name to make lookups consistent. */ + group = apr_pstrcat(cb->parser_pool, "@", option->data, SVN_VA_NULL); + group_len = option->len + 1; + if (apr_hash_get(cb->parsed_groups, group, group_len)) + { + if (cb->parsing_groups) + return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Can't override definition" + " of global group '%s'"), + group); + else + return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Can't override definition" + " of group '%s'"), + group); + } + + /* We store the whole group definition, so that we can use the + temporary groups in the baton hash later to fully expand group + memberships. + At this point, we can finally internalize the group name. */ + apr_hash_set(cb->parsed_groups, + intern_string(cb, group, group_len), group_len, + svn_cstring_split(value->data, ",", TRUE, cb->parser_pool)); + + return SVN_NO_ERROR; +} + + +/* Remove escape sequences in-place. */ +static void +unescape_in_place(svn_stringbuf_t *buf) +{ + char *p = buf->data; + apr_size_t i; + + /* Skip the string up to the first escape sequence. */ + for (i = 0; i < buf->len; ++i) + { + if (*p == '\\') + break; + ++p; + } + + if (i < buf->len) + { + /* Unescape the remainder of the string. */ + svn_boolean_t escape = TRUE; + const char *q; + + for (q = p + 1, ++i; i < buf->len; ++i) + { + if (escape) + { + *p++ = *q++; + escape = FALSE; + } + else if (*q == '\\') + { + ++q; + escape = TRUE; + } + else + *p++ = *q++; + } + + /* A trailing backslash is literal, so make it part of the pattern. */ + if (escape) + *p++ = '\\'; + *p = '\0'; + buf->len = p - buf->data; + } +} + + +/* Internalize a pattern. */ +static void +intern_pattern(ctor_baton_t *cb, + svn_string_t *pattern, + const char *string, + apr_size_t len) +{ + pattern->data = intern_string(cb, string, len); + pattern->len = len; +} + + +/* Parse a rule path PATH up to PATH_LEN into *RULE. + If GLOB is TRUE, treat PATH as possibly containing wildcards. + SECTION is the whole rule in the authz file. + Use pools and buffers from CB to do the obvious thing. */ +static svn_error_t * +parse_rule_path(authz_rule_t *rule, + ctor_baton_t *cb, + svn_boolean_t glob, + const char *path, + apr_size_t path_len, + const char *section) +{ + svn_stringbuf_t *const pattern = cb->rule_string_buffer; + const char *const path_end = path + path_len; + authz_rule_segment_t *segment; + const char *start; + const char *end; + int nseg; + + SVN_ERR_ASSERT(*path == '/'); + + nseg = 0; + for (start = path; start != path_end; start = end) + { + apr_size_t pattern_len; + + /* Skip the leading slash and find the end of the segment. */ + end = memchr(++start, '/', path_len - 1); + if (!end) + end = path_end; + + pattern_len = end - start; + path_len -= pattern_len + 1; + + if (pattern_len == 0) + { + if (nseg == 0) + { + /* This is an empty (root) path. */ + rule->len = 0; + rule->path = NULL; + return SVN_NO_ERROR; + } + + /* A path with two consecutive slashes is not canonical. */ + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, + svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Found empty name in authz rule path")), + _("Non-canonical path '%s' in authz rule [%s]"), + path, section); + } + + /* A path with . or .. segments is not canonical. */ + if (*start == '.' + && (pattern_len == 1 + || (pattern_len == 2 && start[1] == '.'))) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, + (end == start + 1 + ? svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Found '.' in authz rule path")) + : svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Found '..' in authz rule path"))), + _("Non-canonical path '%s' in authz rule [%s]"), + path, section); + + /* Make space for the current segment. */ + ++nseg; + svn_membuf__resize(&cb->rule_path_buffer, nseg * sizeof(*segment)); + segment = cb->rule_path_buffer.data; + segment += (nseg - 1); + + if (!glob) + { + /* Trivial case: this is not a glob rule, so every segment + is a literal match. */ + segment->kind = authz_rule_literal; + intern_pattern(cb, &segment->pattern, start, pattern_len); + continue; + } + + /* Copy the segment into the temporary buffer. */ + svn_stringbuf_setempty(pattern); + svn_stringbuf_appendbytes(pattern, start, pattern_len); + + if (0 == apr_fnmatch_test(pattern->data)) + { + /* It's a literal match after all. */ + segment->kind = authz_rule_literal; + unescape_in_place(pattern); + intern_pattern(cb, &segment->pattern, pattern->data, pattern->len); + continue; + } + + if (*pattern->data == '*') + { + if (pattern->len == 1 + || (pattern->len == 2 && pattern->data[1] == '*')) + { + /* Process * and **, applying normalization as per + https://wiki.apache.org/subversion/AuthzImprovements. */ + + authz_rule_segment_t *const prev = + (nseg > 1 ? segment - 1 : NULL); + + if (pattern_len == 1) + { + /* This is a *. Replace **|* with *|**. */ + if (prev && prev->kind == authz_rule_any_recursive) + { + prev->kind = authz_rule_any_segment; + segment->kind = authz_rule_any_recursive; + } + else + segment->kind = authz_rule_any_segment; + } + else + { + /* This is a **. Replace **|** with a single **. */ + if (prev && prev->kind == authz_rule_any_recursive) + { + /* Simply drop the redundant new segment. */ + --nseg; + continue; + } + else + segment->kind = authz_rule_any_recursive; + } + + segment->pattern.data = interned_empty_string; + segment->pattern.len = 0; + continue; + } + + /* Maybe it's a suffix match? */ + if (0 == apr_fnmatch_test(pattern->data + 1)) + { + svn_stringbuf_leftchop(pattern, 1); + segment->kind = authz_rule_suffix; + unescape_in_place(pattern); + svn_authz__reverse_string(pattern->data, pattern->len); + intern_pattern(cb, &segment->pattern, + pattern->data, pattern->len); + continue; + } + } + + if (pattern->data[pattern->len - 1] == '*') + { + /* Might be a prefix match. Note that because of the + previous test, we already know that the pattern is longer + than one character. */ + if (pattern->data[pattern->len - 2] != '\\') + { + /* OK, the * wasn't escaped. Chop off the wildcard. */ + svn_stringbuf_chop(pattern, 1); + if (0 == apr_fnmatch_test(pattern->data)) + { + segment->kind = authz_rule_prefix; + unescape_in_place(pattern); + intern_pattern(cb, &segment->pattern, + pattern->data, pattern->len); + continue; + } + + /* Restore the wildcard since it was not a prefix match. */ + svn_stringbuf_appendbyte(pattern, '*'); + } + } + + /* It's a generic fnmatch pattern. */ + segment->kind = authz_rule_fnmatch; + intern_pattern(cb, &segment->pattern, pattern->data, pattern->len); + } + + SVN_ERR_ASSERT(nseg > 0); + + /* Copy the temporary segments array into the result pool. */ + { + const apr_size_t path_size = nseg * sizeof(*segment); + SVN_ERR_ASSERT(path_size <= cb->rule_path_buffer.size); + + rule->len = nseg; + rule->path = apr_palloc(cb->authz->pool, path_size); + memcpy(rule->path, cb->rule_path_buffer.data, path_size); + } + + return SVN_NO_ERROR; +} + + +/* Check that the parsed RULE is unique within the authz file. + With the introduction of wildcards, just looking at the SECTION + names is not sufficient to determine uniqueness. + Use pools and buffers from CB to do the obvious thing. */ +static svn_error_t * +check_unique_rule(ctor_baton_t *cb, + const authz_rule_t *rule, + const char *section) +{ + svn_stringbuf_t *const buf = cb->rule_string_buffer; + const char *exists; + int i; + + /* Construct the key for this rule */ + svn_stringbuf_setempty(buf); + svn_stringbuf_appendcstr(buf, rule->repos); + svn_stringbuf_appendbyte(buf, '\n'); + + for (i = 0; i < rule->len; ++i) + { + authz_rule_segment_t *const seg = &rule->path[i]; + svn_stringbuf_appendbyte(buf, '@' + seg->kind); + svn_stringbuf_appendbytes(buf, seg->pattern.data, seg->pattern.len); + svn_stringbuf_appendbyte(buf, '\n'); + } + + /* Check if the section exists. */ + exists = apr_hash_get(cb->parsed_rules, buf->data, buf->len); + if (exists) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Section [%s] describes the same rule as section [%s]"), + section, exists); + + /* Insert the rule into the known rules set. */ + apr_hash_set(cb->parsed_rules, + apr_pstrmemdup(cb->parser_pool, buf->data, buf->len), + buf->len, + apr_pstrdup(cb->parser_pool, section)); + + return SVN_NO_ERROR; +} + + +/* Constructor callback: Starts a rule or [aliases] section. */ +static svn_error_t * +rules_open_section(void *baton, svn_stringbuf_t *section) +{ + ctor_baton_t *const cb = baton; + const char *rule = section->data; + apr_size_t rule_len = section->len; + svn_boolean_t glob; + const char *endp; + parsed_acl_t acl; + + SVN_ERR(check_open_section(cb, section)); + + /* Parse rule property tokens. */ + if (*rule != ':') + glob = FALSE; + else + { + /* This must be a wildcard rule. */ + apr_size_t token_len; + + ++rule; --rule_len; + endp = memchr(rule, ':', rule_len); + if (!endp) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Empty repository name in authz rule [%s]"), + section->data); + + /* Note: the size of glob_rule_token includes the NUL terminator. */ + token_len = endp - rule; + if (token_len != sizeof(glob_rule_token) - 1 + || memcmp(rule, glob_rule_token, token_len)) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Invalid type token '%s' in authz rule [%s]"), + apr_pstrmemdup(cb->parser_pool, rule, token_len), + section->data); + + glob = TRUE; + rule = endp + 1; + rule_len -= token_len + 1; + } + + /* Parse the repository name. */ + endp = (*rule == '/' ? NULL : memchr(rule, ':', rule_len)); + if (!endp) + acl.acl.rule.repos = interned_empty_string; + else + { + const apr_size_t repos_len = endp - rule; + + /* The rule contains a repository name. */ + if (0 == repos_len) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Empty repository name in authz rule [%s]"), + section->data); + + acl.acl.rule.repos = intern_string(cb, rule, repos_len); + rule = endp + 1; + rule_len -= repos_len + 1; + } + + /* Parse the actual rule. */ + if (*rule == '/') + { + SVN_ERR(parse_rule_path(&acl.acl.rule, cb, glob, rule, rule_len, + section->data)); + SVN_ERR(check_unique_rule(cb, &acl.acl.rule, section->data)); + } + else if (0 == strcmp(section->data, aliases_section)) + { + cb->in_aliases = TRUE; + return SVN_NO_ERROR; + } + else + { + /* This must be the [groups] section. */ + return groups_open_section(cb, section); + } + + acl.acl.sequence_number = cb->parsed_acls->nelts; + acl.acl.anon_access = authz_access_none; + acl.acl.has_anon_access = FALSE; + acl.acl.authn_access = authz_access_none; + acl.acl.has_authn_access = FALSE; + acl.acl.user_access = NULL; + + acl.aces = svn_hash__make(cb->parser_pool); + acl.alias_aces = svn_hash__make(cb->parser_pool); + + cb->current_acl = &APR_ARRAY_PUSH(cb->parsed_acls, parsed_acl_t); + *cb->current_acl = acl; + return SVN_NO_ERROR; +} + + +/* Parses an alias declaration. The definition (username) of the + alias will always be interned. */ +static svn_error_t * +add_alias_definition(ctor_baton_t *cb, + svn_stringbuf_t *option, svn_stringbuf_t *value) +{ + const char *alias; + apr_size_t alias_len; + const char *user; + + if (strchr("@$&*~", *option->data)) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Alias name '%s' may not begin with '%c'"), + option->data, *option->data); + + /* Decorate the name to make lookups consistent. */ + alias = apr_pstrcat(cb->parser_pool, "&", option->data, SVN_VA_NULL); + alias_len = option->len + 1; + if (apr_hash_get(cb->parsed_aliases, alias, alias_len)) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Can't override definition of alias '%s'"), + alias); + + user = intern_string(cb, value->data, value->len); + apr_hash_set(cb->parsed_aliases, alias, alias_len, user); + + /* Prepare the global rights struct for this user. */ + prepare_global_rights(cb, user); + return SVN_NO_ERROR; +} + +/* Parses an access entry. Groups and users in access entry names will + always be interned, aliases will never be. */ +static svn_error_t * +add_access_entry(ctor_baton_t *cb, svn_stringbuf_t *section, + svn_stringbuf_t *option, svn_stringbuf_t *value) +{ + parsed_acl_t *const acl = cb->current_acl; + const char *name = option->data; + apr_size_t name_len = option->len; + const svn_boolean_t inverted = (*name == '~'); + svn_boolean_t anonymous = FALSE; + svn_boolean_t authenticated = FALSE; + authz_access_t access = authz_access_none; + authz_ace_t *ace; + int i; + + SVN_ERR_ASSERT(acl != NULL); + + if (inverted) + { + ++name; + --name_len; + } + + /* Determine the access entry type. */ + switch (*name) + { + case '~': + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Access entry '%s' has more than one inversion;" + " double negatives are not permitted"), + option->data); + break; + + case '*': + if (name_len != 1) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Access entry '%s' is not valid;" + " it must be a single '*'"), + option->data); + + if (inverted) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Access entry '~*' will never match")); + + anonymous = TRUE; + authenticated = TRUE; + break; + + case '$': + if (0 == strcmp(name, anon_access_token)) + { + if (inverted) + authenticated = TRUE; + else + anonymous = TRUE; + } + else if (0 == strcmp(name, authn_access_token)) + { + if (inverted) + anonymous = TRUE; + else + authenticated = TRUE; + } + else + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Access entry token '%s' is not valid;" + " should be '%s' or '%s'"), + option->data, anon_access_token, authn_access_token); + break; + + default: + /* A username, group name or alias. */; + } + + /* Parse the access rights. */ + for (i = 0; i < value->len; ++i) + { + const char access_code = value->data[i]; + switch (access_code) + { + case 'r': + access |= authz_access_read_flag; + break; + + case 'w': + access |= authz_access_write_flag; + break; + + default: + if (!svn_ctype_isspace(access_code)) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("The access mode '%c' in access entry '%s'" + " of rule [%s] is not valid"), + access_code, option->data, section->data); + } + } + + /* We do not support write-only access. */ + if ((access & authz_access_write_flag) && !(access & authz_access_read_flag)) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Write-only access entry '%s' of rule [%s] is not valid"), + option->data, section->data); + + /* Update the parsed ACL with this access entry. */ + if (anonymous || authenticated) + { + if (anonymous) + { + acl->acl.has_anon_access = TRUE; + acl->acl.anon_access |= access; + } + if (authenticated) + { + acl->acl.has_authn_access = TRUE; + acl->acl.authn_access |= access; + } + } + else + { + /* The inversion tag must be part of the key in the hash + table, otherwise we can't tell regular and inverted + entries appart. */ + const char *key = (inverted ? name - 1 : name); + const apr_size_t key_len = (inverted ? name_len + 1 : name_len); + const svn_boolean_t aliased = (*name == '&'); + apr_hash_t *aces = (aliased ? acl->alias_aces : acl->aces); + + ace = apr_hash_get(aces, key, key_len); + if (ace) + ace->access |= access; + else + { + ace = apr_palloc(cb->parser_pool, sizeof(*ace)); + ace->name = (aliased + ? apr_pstrmemdup(cb->parser_pool, name, name_len) + : intern_string(cb, name, name_len)); + ace->members = NULL; + ace->inverted = inverted; + ace->access = access; + + key = (inverted + ? apr_pstrmemdup(cb->parser_pool, key, key_len) + : ace->name); + apr_hash_set(aces, key, key_len, ace); + + /* Prepare the global rights struct for this user. */ + if (!aliased && *ace->name != '@') + prepare_global_rights(cb, ace->name); + } + } + + return SVN_NO_ERROR; +} + +/* Constructor callback: Parse a rule, alias or group delcaration. */ +static svn_error_t * +rules_add_value(void *baton, svn_stringbuf_t *section, + svn_stringbuf_t *option, svn_stringbuf_t *value) +{ + ctor_baton_t *const cb = baton; + + if (cb->in_groups) + return groups_add_value(baton, section, option, value); + + if (cb->in_aliases) + return add_alias_definition(cb, option, value); + + return add_access_entry(cb, section, option, value); +} + + +/* Constructor callback: Close a section. */ +static svn_error_t * +close_section(void *baton, svn_stringbuf_t *section) +{ + ctor_baton_t *const cb = baton; + + SVN_ERR_ASSERT(0 == strcmp(cb->section, section->data)); + cb->section = NULL; + cb->current_acl = NULL; + cb->in_groups = FALSE; + cb->in_aliases = FALSE; + return SVN_NO_ERROR; +} + + +/* Add a user to GROUP. + GROUP is never internalized, but USER always is. */ +static void +add_to_group(ctor_baton_t *cb, const char *group, const char *user) +{ + apr_hash_t *members = svn_hash_gets(cb->expanded_groups, group); + if (!members) + { + group = intern_string(cb, group, -1); + members = svn_hash__make(cb->authz->pool); + svn_hash_sets(cb->expanded_groups, group, members); + } + svn_hash_sets(members, user, interned_empty_string); +} + + +/* Hash iterator for expanding group definitions. + WARNING: This function is recursive! */ +static svn_error_t * +expand_group_callback(void *baton, + const void *key, + apr_ssize_t klen, + void *value, + apr_pool_t *scratch_pool) +{ + ctor_baton_t *const cb = baton; + const char *const group = key; + apr_array_header_t *members = value; + + int i; + for (i = 0; i < members->nelts; ++i) + { + const char *member = APR_ARRAY_IDX(members, i, const char*); + if (0 == strcmp(member, group)) + return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Recursive definition of group '%s'"), + group); + + if (*member == '&') + { + /* Add expanded alias to the group. + N.B.: the user name is already internalized. */ + const char *user = svn_hash_gets(cb->parsed_aliases, member); + if (!user) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Alias '%s' was never defined"), + member); + + add_to_group(cb, group, user); + } + else if (*member != '@') + { + /* Add the member to the group. */ + const char *user = intern_string(cb, member, -1); + add_to_group(cb, group, user); + + /* Prepare the global rights struct for this user. */ + prepare_global_rights(cb, user); + } + else + { + /* Recursively expand the group membership */ + members = svn_hash_gets(cb->parsed_groups, member); + if (!members) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Undefined group '%s'"), + member); + SVN_ERR(expand_group_callback(cb, key, klen, + members, scratch_pool)); + } + } + return SVN_NO_ERROR; +} + + +/* Hash iteration baton for merge_alias_ace. */ +typedef struct merge_alias_baton_t +{ + apr_hash_t *aces; + ctor_baton_t *cb; +} merge_alias_baton_t; + +/* Hash iterator for expanding and mergina alias-based ACEs + into the user/group-based ACEs. */ +static svn_error_t * +merge_alias_ace(void *baton, + const void *key, + apr_ssize_t klen, + void *value, + apr_pool_t *scratch_pool) +{ + merge_alias_baton_t *const mab = baton; + authz_ace_t *aliased_ace = value; + const char *alias = aliased_ace->name; + const char *unaliased_key; + const char *user; + authz_ace_t *ace; + + user = svn_hash_gets(mab->cb->parsed_aliases, alias); + if (!user) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Alias '%s' was never defined"), + alias); + + /* N.B.: The user name is always internalized, + but the inverted key may not be. */ + if (!aliased_ace->inverted) + unaliased_key = user; + else + { + unaliased_key = apr_pstrcat(mab->cb->parser_pool, + "~", user, SVN_VA_NULL); + unaliased_key = intern_string(mab->cb, unaliased_key, -1); + } + + ace = svn_hash_gets(mab->aces, unaliased_key); + if (!ace) + { + aliased_ace->name = user; + svn_hash_sets(mab->aces, unaliased_key, aliased_ace); + } + else + { + SVN_ERR_ASSERT(!ace->inverted == !aliased_ace->inverted); + ace->access |= aliased_ace->access; + } + + return SVN_NO_ERROR; +} + + +/* Hash iteration baton for array_insert_ace. */ +typedef struct insert_ace_baton_t +{ + apr_array_header_t *ace_array; + ctor_baton_t *cb; +} insert_ace_baton_t; + +/* Hash iterator, inserts an ACE into the ACLs array. */ +static svn_error_t * +array_insert_ace(void *baton, + const void *key, + apr_ssize_t klen, + void *value, + apr_pool_t *scratch_pool) +{ + insert_ace_baton_t *iab = baton; + authz_ace_t *ace = value; + + /* Add group membership info to the ACE. */ + if (*ace->name == '@') + { + SVN_ERR_ASSERT(ace->members == NULL); + ace->members = svn_hash_gets(iab->cb->expanded_groups, ace->name); + if (!ace->members) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Access entry refers to undefined group '%s'"), + ace->name); + } + + APR_ARRAY_PUSH(iab->ace_array, authz_ace_t) = *ace; + return SVN_NO_ERROR; +} + + +/* Update accumulated RIGHTS from ACCESS. */ +static void +update_rights(authz_rights_t *rights, + authz_access_t access) +{ + rights->min_access &= access; + rights->max_access |= access; +} + + +/* Update a global RIGHTS based on REPOS and ACCESS. */ +static void +update_global_rights(authz_global_rights_t *gr, + const char *repos, + authz_access_t access) +{ + update_rights(&gr->all_repos_rights, access); + if (0 == strcmp(repos, AUTHZ_ANY_REPOSITORY)) + update_rights(&gr->any_repos_rights, access); + else + { + authz_rights_t *rights = svn_hash_gets(gr->per_repos_rights, repos); + if (rights) + update_rights(rights, access); + else + { + rights = apr_palloc(apr_hash_pool_get(gr->per_repos_rights), + sizeof(*rights)); + init_rights(rights); + update_rights(rights, access); + svn_hash_sets(gr->per_repos_rights, repos, rights); + } + } +} + + +/* Hash iterator to update global per-user rights from an ACL. */ +static svn_error_t * +update_user_rights(void *baton, + const void *key, + apr_ssize_t klen, + void *value, + apr_pool_t *scratch_pool) +{ + const authz_acl_t *const acl = baton; + const char *const user = key; + authz_global_rights_t *const gr = value; + authz_access_t access; + svn_boolean_t has_access = + svn_authz__get_acl_access(&access, acl, user, acl->rule.repos); + + if (has_access) + update_global_rights(gr, acl->rule.repos, access); + return SVN_NO_ERROR; +} + + +/* List iterator, expands/merges a parsed ACL into its final form and + appends it to the authz info's ACL array. */ +static svn_error_t * +expand_acl_callback(void *baton, + void *item, + apr_pool_t *scratch_pool) +{ + ctor_baton_t *const cb = baton; + parsed_acl_t *const pacl = item; + authz_acl_t *const acl = &pacl->acl; + + /* Expand and merge the aliased ACEs. */ + if (apr_hash_count(pacl->alias_aces)) + { + merge_alias_baton_t mab; + mab.aces = pacl->aces; + mab.cb = cb; + SVN_ERR(svn_iter_apr_hash(NULL, pacl->alias_aces, + merge_alias_ace, &mab, scratch_pool)); + } + + /* Make an array from the merged hashes. */ + acl->user_access = + apr_array_make(cb->authz->pool, apr_hash_count(pacl->aces), + sizeof(authz_ace_t)); + { + insert_ace_baton_t iab; + iab.ace_array = acl->user_access; + iab.cb = cb; + SVN_ERR(svn_iter_apr_hash(NULL, pacl->aces, + array_insert_ace, &iab, scratch_pool)); + } + + /* Store the completed ACL into authz. */ + APR_ARRAY_PUSH(cb->authz->acls, authz_acl_t) = *acl; + + /* Update global access rights for this ACL. */ + if (acl->has_anon_access) + { + cb->authz->has_anon_rights = TRUE; + update_global_rights(&cb->authz->anon_rights, + acl->rule.repos, acl->anon_access); + } + if (acl->has_authn_access) + { + cb->authz->has_authn_rights = TRUE; + update_global_rights(&cb->authz->authn_rights, + acl->rule.repos, acl->authn_access); + } + SVN_ERR(svn_iter_apr_hash(NULL, cb->authz->user_rights, + update_user_rights, acl, scratch_pool)); + return SVN_NO_ERROR; +} + + +/* Compare two ACLs in rule lexical order, then repository order, then + order of definition. This ensures that our default ACL is always + first in the sorted array. */ +static int +compare_parsed_acls(const void *va, const void *vb) +{ + const parsed_acl_t *const a = va; + const parsed_acl_t *const b = vb; + + int cmp = svn_authz__compare_rules(&a->acl.rule, &b->acl.rule); + if (cmp == 0) + cmp = a->acl.sequence_number - b->acl.sequence_number; + return cmp; +} + + +svn_error_t * +svn_authz__parse(authz_full_t **authz, + svn_stream_t *rules, + svn_stream_t *groups, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + ctor_baton_t *const cb = create_ctor_baton(result_pool, scratch_pool); + + /* + * Pass 1: Parse the authz file. + */ + SVN_ERR(svn_config__parse_stream(rules, + svn_config__constructor_create( + rules_open_section, + close_section, + rules_add_value, + cb->parser_pool), + cb, cb->parser_pool)); + + /* + * Pass 1.6487: Parse the global groups file. + */ + if (groups) + { + /* Check that the authz file did not contain any groups. */ + if (0 != apr_hash_count(cb->parsed_groups)) + return svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + ("Authz file cannot contain any groups" + " when global groups are being used.")); + + apr_hash_clear(cb->sections); + cb->parsing_groups = TRUE; + SVN_ERR(svn_config__parse_stream(groups, + svn_config__constructor_create( + groups_open_section, + close_section, + groups_add_value, + cb->parser_pool), + cb, cb->parser_pool)); + } + + /* + * Pass 2: Expand groups and construct the final svn_authz_t. + */ + cb->expanded_groups = svn_hash__make(cb->parser_pool); + SVN_ERR(svn_iter_apr_hash(NULL, cb->parsed_groups, + expand_group_callback, cb, cb->parser_pool)); + + + /* Sort the parsed ACLs in rule lexical order and pop off the + default global ACL iff an equivalent ACL was defined in the authz + file. */ + if (cb->parsed_acls->nelts > 1) + { + parsed_acl_t *defacl; + parsed_acl_t *nxtacl; + + svn_sort__array(cb->parsed_acls, compare_parsed_acls); + defacl = &APR_ARRAY_IDX(cb->parsed_acls, 0, parsed_acl_t); + nxtacl = &APR_ARRAY_IDX(cb->parsed_acls, 1, parsed_acl_t); + + /* If the first ACL is not our default thingamajig, there's a + bug in our comparator. */ + SVN_ERR_ASSERT( + defacl->acl.sequence_number == 0 && defacl->acl.rule.len == 0 + && 0 == strcmp(defacl->acl.rule.repos, AUTHZ_ANY_REPOSITORY)); + + /* Pop the default ACL off the array if another equivalent + exists, after merging the default rights. */ + if (0 == svn_authz__compare_rules(&defacl->acl.rule, &nxtacl->acl.rule)) + { + nxtacl->acl.has_anon_access = TRUE; + nxtacl->acl.has_authn_access = TRUE; + cb->parsed_acls->elts = (char*)(nxtacl); + --cb->parsed_acls->nelts; + } + } + + cb->authz->acls = apr_array_make(cb->authz->pool, cb->parsed_acls->nelts, + sizeof(authz_acl_t)); + SVN_ERR(svn_iter_apr_array(NULL, cb->parsed_acls, + expand_acl_callback, cb, cb->parser_pool)); + + *authz = cb->authz; + apr_pool_destroy(cb->parser_pool); + return SVN_NO_ERROR; +} + + +void +svn_authz__reverse_string(char *string, apr_size_t len) +{ + char *left = string; + char *right = string + len - 1; + for (; left < right; ++left, --right) + { + char c = *left; + *left = *right; + *right = c; + } +} + + +int +svn_authz__compare_paths(const authz_rule_t *a, const authz_rule_t *b) +{ + const int min_len = (a->len > b->len ? b->len : a->len); + int i; + + for (i = 0; i < min_len; ++i) + { + int cmp = a->path[i].kind - b->path[i].kind; + if (0 == cmp) + { + const char *const aseg = a->path[i].pattern.data; + const char *const bseg = b->path[i].pattern.data; + + /* Exploit the fact that segment patterns are interned. */ + if (aseg != bseg) + cmp = strcmp(aseg, bseg); + else + cmp = 0; + } + if (0 != cmp) + return cmp; + } + + /* Sort shorter rules first. */ + if (a->len != b->len) + return a->len - b->len; + + return 0; +} + +int +svn_authz__compare_rules(const authz_rule_t *a, const authz_rule_t *b) +{ + int diff = svn_authz__compare_paths(a, b); + if (diff) + return diff; + + /* Repository names are interned, too. */ + if (a->repos != b->repos) + return strcmp(a->repos, b->repos); + + return 0; +} |