/* authz-test.c --- tests for authorization system
 *
 * ====================================================================
 *    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 "svn_pools.h"
#include "svn_iter.h"
#include "svn_hash.h"
#include "private/svn_subr_private.h"

#include "../../libsvn_repos/authz.h"

#include "../svn_test.h"

/* Used to terminate lines in large multi-line string literals. */
#define NL APR_EOL_STR

static svn_error_t *
print_group_member(void *baton,
                   const void *key, apr_ssize_t klen, void *val,
                   apr_pool_t *pool)
{
  svn_boolean_t *first = baton;
  const char *member = key;
  printf("%s%s", (*first ? "" : ", "), member);
  *first = FALSE;
  return SVN_NO_ERROR;
}


static svn_error_t *
print_group(void *baton,
            const void *key, apr_ssize_t klen, void *val,
            apr_pool_t *pool)
{
  const char *group = key;
  apr_hash_t *members = val;
  svn_boolean_t first = TRUE;
  svn_error_t *err;

  printf("   %s = ", group);
  err = svn_iter_apr_hash(NULL, members,
                          print_group_member, &first, pool);
  printf("\n");

  return err;
}


static const char *
access_string(authz_access_t access)
{
  switch (access & authz_access_write)
    {
    case authz_access_none: return ""; break;
    case authz_access_read_flag: return "r"; break;
    case authz_access_write_flag: return "w"; break;
    default:
      return "rw";
    }
}

static svn_error_t *
print_repos_rights(void *baton,
                   const void *key, apr_ssize_t klen,
                   void *val,
                   apr_pool_t *pool)
{
  const char *repos = key;
  authz_rights_t *rights = val;
  printf("      %s = all:%s  some:%s\n", repos,
         access_string(rights->min_access),
         access_string(rights->max_access));
  return SVN_NO_ERROR;
}

static svn_error_t *
print_user_rights(void *baton, const void *key, apr_ssize_t klen,
                  void *val,
                  apr_pool_t *pool)
{
  authz_global_rights_t *gr = val;

  printf("   %s\n", gr->user);
  printf("      [all] = all:%s  some:%s\n",
         access_string(gr->all_repos_rights.min_access),
         access_string(gr->all_repos_rights.max_access));
  printf("      [any] = all:%s  some:%s\n",
         access_string(gr->any_repos_rights.min_access),
         access_string(gr->any_repos_rights.max_access));
  SVN_ERR(svn_iter_apr_hash(NULL, gr->per_repos_rights,
                            print_repos_rights, NULL, pool));
  return SVN_NO_ERROR;
}

static const char*
rule_string(authz_rule_t* rule, apr_pool_t *pool)
{
  svn_stringbuf_t *str;
  int i;

  if (rule->len == 0)
    return "/";

  str = svn_stringbuf_create_empty(pool);

  for (i = 0; i < rule->len; ++i)
    {
      authz_rule_segment_t *segment = &rule->path[i];

      switch(segment->kind)
        {
        case authz_rule_any_segment:
          svn_stringbuf_appendcstr(str, "/*");
          break;

        case authz_rule_any_recursive:
          svn_stringbuf_appendcstr(str, "/**");
          break;

        case authz_rule_prefix:
          svn_stringbuf_appendcstr(str, "/#");
          svn_stringbuf_appendcstr(str, segment->pattern.data);
          svn_stringbuf_appendbyte(str, '*');
          break;

        case authz_rule_suffix:
          svn_stringbuf_appendcstr(str, "/#*");
          svn_stringbuf_appendcstr(str, segment->pattern.data);
          svn_authz__reverse_string(
              str->data + str->len - segment->pattern.len,
              segment->pattern.len);
          break;

        case authz_rule_fnmatch:
          svn_stringbuf_appendcstr(str, "/%");
          svn_stringbuf_appendcstr(str, segment->pattern.data);
          break;

        default:                /* literal */
          svn_stringbuf_appendcstr(str, "//");
          svn_stringbuf_appendcstr(str, segment->pattern.data);
        }
    }
  return str->data;
}


static svn_boolean_t
has_glob(authz_rule_t* rule)
{
  int i;
  for (i = 0; i < rule->len; ++i)
    {
      authz_rule_segment_t *segment = &rule->path[i];
      if (segment->kind != authz_rule_literal)
        return TRUE;
    }
  return FALSE;
}


static svn_error_t *
test_authz_parse(const svn_test_opts_t *opts,
                 apr_pool_t *pool)
{
  const char *srcdir;
  const char *rules_path;
  apr_file_t *rules_file;
  svn_stream_t *rules;
  const char *groups_path;
  apr_file_t *groups_file;
  svn_stream_t *groups;
  authz_full_t *authz;
  apr_hash_t *groupdefs = svn_hash__make(pool);
  int i;

  const char *check_user = "wunga";
  const char *check_repo = "bloop";
  authz_rights_t global_rights;
  svn_boolean_t global_explicit;


  SVN_ERR(svn_test_get_srcdir(&srcdir, opts, pool));
  rules_path = svn_dirent_join(srcdir, "authz.rules", pool);
  groups_path = svn_dirent_join(srcdir, "authz.groups", pool);

  SVN_ERR(svn_io_file_open(&rules_file, rules_path,
                           APR_READ, APR_OS_DEFAULT,
                           pool));
  rules = svn_stream_from_aprfile2(rules_file, FALSE, pool);
  SVN_ERR(svn_io_file_open(&groups_file, groups_path,
                           APR_READ, APR_OS_DEFAULT,
                           pool));
  groups = svn_stream_from_aprfile2(groups_file, FALSE, pool);
  SVN_ERR(svn_authz__parse(&authz, rules, groups, NULL, NULL, pool, pool));

  printf("Access check for ('%s', '%s')\n", check_user, check_repo);

  global_explicit = svn_authz__get_global_rights(&global_rights, authz,
                                                 check_user, check_repo);
  printf("Global rights: min=%s, max=%s (%s)\n\n",
         access_string(global_rights.min_access),
         access_string(global_rights.max_access),
         (global_explicit ? "explicit" : "implicit"));

  printf("[rules]\n");
  for (i = 0; i < authz->acls->nelts; ++i)
    {
      authz_acl_t *acl = &APR_ARRAY_IDX(authz->acls, i, authz_acl_t);
      const authz_access_t all_access =
        (acl->anon_access & acl->authn_access);
      authz_access_t access;
      svn_boolean_t has_access =
        svn_authz__get_acl_access(&access, acl, check_user, check_repo);
      int j;

      printf("%s%s%s   Sequence:   %d\n"
             "   Repository: [%s]\n"
             "   Rule:  %s[%s]\n",
             (has_access ? "Match = " : ""),
             (has_access ? access_string(access) : ""),
             (has_access ? "\n" : ""),
             acl->sequence_number,
             acl->rule.repos,
             (has_glob(&acl->rule) ? "glob:" : "     "),
             rule_string(&acl->rule, pool));

      if (acl->has_anon_access && acl->has_authn_access)
        printf("       * = %s\n", access_string(all_access));

      if (acl->has_anon_access
          && (acl->anon_access & ~all_access) != svn_authz_none)
        printf("       $anonymous = %s\n",
               access_string(acl->anon_access));

      if (acl->has_authn_access
          && (acl->authn_access & ~all_access) != svn_authz_none)
        printf("       $authenticated = %s\n",
               access_string(acl->authn_access));

      for (j = 0; j < acl->user_access->nelts; ++j)
        {
          authz_ace_t *ace = &APR_ARRAY_IDX(acl->user_access, j, authz_ace_t);
          printf("      %c%s = %s\n",
                 (ace->inverted ? '~' : ' '),
                 ace->name, access_string(ace->access));
          if (ace->members)
            svn_hash_sets(groupdefs, ace->name, ace->members);
        }
      printf("\n\n");
    }

  printf("[groups]\n");
  SVN_ERR(svn_iter_apr_hash(NULL, groupdefs,
                            print_group, NULL, pool));
  printf("\n\n");

  printf("[users]\n");
  if (authz->has_anon_rights)
    SVN_ERR(print_user_rights(NULL, NULL, 0, &authz->anon_rights, pool));
  if (authz->has_authn_rights)
    SVN_ERR(print_user_rights(NULL, NULL, 0, &authz->authn_rights, pool));
  SVN_ERR(svn_iter_apr_hash(NULL, authz->user_rights,
                            print_user_rights, NULL, pool));
  printf("\n\n");

  return SVN_NO_ERROR;
}

typedef struct global_right_text_case_t
{
  const char *repos;
  const char *user;
  authz_rights_t rights;
  svn_boolean_t found;
} global_right_text_case_t;

static svn_error_t *
run_global_rights_tests(const char *contents,
                        const global_right_text_case_t *test_cases,
                        apr_pool_t *pool)
{
  svn_authz_t *authz;

  svn_stringbuf_t *buffer = svn_stringbuf_create(contents, pool);
  svn_stream_t *stream = svn_stream_from_stringbuf(buffer, pool);
  SVN_ERR(svn_repos_authz_parse2(&authz, stream, NULL, NULL, NULL, pool, pool));

  for (; test_cases->repos; ++test_cases)
    {
      authz_rights_t rights = { authz_access_write, authz_access_none };
      svn_boolean_t found = svn_authz__get_global_rights(&rights, authz->full,
                                                         test_cases->user,
                                                         test_cases->repos);

      printf("%s %s {%d %d} %d => {%d %d} %d\n",
        test_cases->repos, test_cases->user,
        test_cases->rights.min_access, test_cases->rights.max_access,
        test_cases->found, rights.min_access, rights.max_access, found);
      SVN_TEST_ASSERT(found == test_cases->found);
      SVN_TEST_ASSERT(rights.min_access == test_cases->rights.min_access);
      SVN_TEST_ASSERT(rights.max_access == test_cases->rights.max_access);
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
test_global_rights(apr_pool_t *pool)
{
  const char* authz1 =
    "[/public]"                                                          NL
    "* = r"                                                              NL
    ""                                                                   NL
    "[greek:/A]"                                                         NL
    "userA = rw"                                                         NL
    ""                                                                   NL
    "[repo:/A]"                                                          NL
    "userA = r"                                                          NL
    ""                                                                   NL
    "[repo:/B]"                                                          NL
    "userA = rw"                                                         NL
    ""                                                                   NL
    "[greek:/B]"                                                         NL
    "userB = rw"                                                         NL;

  const global_right_text_case_t test_cases1[] =
    {
      /* Everyone may get read access b/c there might be a "/public" path. */
      {      "",      "", { authz_access_none, authz_access_read  },  TRUE },
      {      "", "userA", { authz_access_none, authz_access_read  },  TRUE },
      {      "", "userB", { authz_access_none, authz_access_read  },  TRUE },
      {      "", "userC", { authz_access_none, authz_access_read  },  TRUE },

      /* Two users do even get write access on some paths in "greek".
       * The root always defaults to n/a due to the default rule. */
      { "greek",      "", { authz_access_none, authz_access_read  }, FALSE },
      { "greek", "userA", { authz_access_none, authz_access_write },  TRUE },
      { "greek", "userB", { authz_access_none, authz_access_write },  TRUE },
      { "greek", "userC", { authz_access_none, authz_access_read  }, FALSE },

      /* One users has write access to some paths in "repo". */
      {  "repo",      "", { authz_access_none, authz_access_read  }, FALSE },
      {  "repo", "userA", { authz_access_none, authz_access_write },  TRUE },
      {  "repo", "userB", { authz_access_none, authz_access_read  }, FALSE },
      {  "repo", "userC", { authz_access_none, authz_access_read  }, FALSE },

      /* For unknown repos, we default to the global settings. */
      {     "X",      "", { authz_access_none, authz_access_read  }, FALSE },
      {     "X", "userA", { authz_access_none, authz_access_read  }, FALSE },
      {     "X", "userB", { authz_access_none, authz_access_read  }, FALSE },
      {     "X", "userC", { authz_access_none, authz_access_read  }, FALSE },

      { NULL }
    };

  const char* authz2 =
    "[/]"                                                                NL
    "userA = r"                                                          NL
    ""                                                                   NL
    "[/public]"                                                          NL
    "userB = rw"                                                         NL
    ""                                                                   NL
    "[repo:/]"                                                           NL
    "userA = rw"                                                         NL;

  const global_right_text_case_t test_cases2[] =
    {
      /* Everyone may get read access b/c there might be a "/public" path. */
      {      "",      "", { authz_access_none, authz_access_none  },  TRUE },
      {      "", "userA", { authz_access_none, authz_access_read  },  TRUE },
      {      "", "userB", { authz_access_none, authz_access_write },  TRUE },
      {      "", "userC", { authz_access_none, authz_access_none  },  TRUE },

      /* Two users do even get write access on some paths in "greek".
       * The root always defaults to n/a due to the default rule. */
      { "greek",      "", { authz_access_none, authz_access_none  }, FALSE },
      { "greek", "userA", { authz_access_none, authz_access_read  }, FALSE },
      { "greek", "userB", { authz_access_none, authz_access_write }, FALSE },
      { "greek", "userC", { authz_access_none, authz_access_none  }, FALSE },

      { NULL }
    };

  const char* authz3 =
    "[/]"                                                                NL
    "userA = r"                                                          NL
    ""                                                                   NL
    "[greek:/public]"                                                    NL
    "userB = rw"                                                         NL
    ""                                                                   NL
    "[repo:/users]"                                                      NL
    "$authenticated = rw"                                                NL;

  const global_right_text_case_t test_cases3[] =
    {
      /* Everyone may get read access b/c there might be a "/public" path. */
      {      "",      "", { authz_access_none, authz_access_none  },  TRUE },
      {      "", "userA", { authz_access_none, authz_access_read  },  TRUE },
      {      "", "userB", { authz_access_none, authz_access_none  },  TRUE },
      {      "", "userC", { authz_access_none, authz_access_none  },  TRUE },

      /* Two users do even get write access on some paths in "greek".
       * The root always defaults to n/a due to the default rule. */
      { "greek",      "", { authz_access_none, authz_access_none  }, FALSE },
      { "greek", "userA", { authz_access_none, authz_access_read  }, FALSE },
      { "greek", "userB", { authz_access_none, authz_access_write },  TRUE },
      { "greek", "userC", { authz_access_none, authz_access_none  }, FALSE },

      /* Two users do even get write access on some paths in "greek".
       * The root always defaults to n/a due to the default rule. */
      {  "repo",      "", { authz_access_none, authz_access_none  }, FALSE },
      {  "repo", "userA", { authz_access_none, authz_access_write },  TRUE },
      {  "repo", "userB", { authz_access_none, authz_access_write },  TRUE },
      {  "repo", "userC", { authz_access_none, authz_access_write },  TRUE },

      { NULL }
    };

  SVN_ERR(run_global_rights_tests(authz1, test_cases1, pool));
  SVN_ERR(run_global_rights_tests(authz2, test_cases2, pool));
  SVN_ERR(run_global_rights_tests(authz3, test_cases3, pool));

  return SVN_NO_ERROR;
}

static svn_error_t *
issue_4741_groups(apr_pool_t *pool)
{
   const char rules[] =
     "[groups]"      NL
     "g1 = userA"    NL
     "g2 = userB"    NL
     "g = @g1, @g2"  NL
     ""              NL
     "[/]"           NL
     "* ="           NL
     "@g = rw"       NL
     ;

   svn_stringbuf_t *buf = svn_stringbuf_create(rules, pool);
   svn_stream_t *stream = svn_stream_from_stringbuf(buf, pool);
   svn_authz_t *authz;
   svn_boolean_t access_granted;

   SVN_ERR(svn_repos_authz_parse2(&authz, stream, NULL, NULL, NULL, pool, pool));

   SVN_ERR(svn_repos_authz_check_access(authz, "repo", "/", "userA",
                                        svn_authz_write, &access_granted,
                                        pool));
   SVN_TEST_ASSERT(access_granted == TRUE);

   SVN_ERR(svn_repos_authz_check_access(authz, "repo", "/", "userB",
                                        svn_authz_write, &access_granted,
                                        pool));
   SVN_TEST_ASSERT(access_granted == TRUE);

   return SVN_NO_ERROR;
}

static svn_error_t *
reposful_reposless_stanzas_inherit(apr_pool_t *pool)
{
  const char rules[] =
    "[groups]"                               NL
    "company = user1, user2, user3"          NL
    "customer = customer1, customer2"        NL
    ""                                       NL
    "# company can read-write on everything" NL
    "[/]"                                    NL
    "@company = rw"                          NL
    ""                                       NL
    "[project1:/]"                           NL
    "@customer = r"                          NL
    ""                                       NL
    "[project2:/]"                           NL;

   svn_stringbuf_t *buf = svn_stringbuf_create(rules, pool);
   svn_stream_t *stream = svn_stream_from_stringbuf(buf, pool);
   svn_authz_t *authz;
   svn_boolean_t access_granted;

   SVN_ERR(svn_repos_authz_parse2(&authz, stream, NULL, NULL, NULL, pool, pool));

   SVN_ERR(svn_repos_authz_check_access(authz, "project1", "/foo", "user1",
                                        svn_authz_write | svn_authz_recursive,
                                        &access_granted,
                                        pool));
   SVN_TEST_ASSERT(access_granted == TRUE);

   return SVN_NO_ERROR;
}

static int max_threads = 4;

static struct svn_test_descriptor_t test_funcs[] =
  {
    SVN_TEST_NULL,
    SVN_TEST_OPTS_PASS(test_authz_parse,
                       "test svn_authz__parse"),
    SVN_TEST_PASS2(test_global_rights,
                   "test svn_authz__get_global_rights"),
    SVN_TEST_PASS2(issue_4741_groups,
                   "issue 4741 groups"),
    SVN_TEST_PASS2(reposful_reposless_stanzas_inherit,
                    "[foo:/] inherits [/]"),
    SVN_TEST_NULL
  };

SVN_TEST_MAIN
