// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab
/*
 * Ceph - scalable distributed file system
 *
 * Copyright (C) 2013 Inktank
 *
 * This is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1, as published by the Free Software
 * Foundation.  See file COPYING.
 *
 */

#include <boost/algorithm/string/predicate.hpp>
#include <boost/config/warning_disable.hpp>
#include <boost/fusion/adapted/struct/adapt_struct.hpp>
#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/fusion/include/std_pair.hpp>
#include <boost/phoenix.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/qi_uint.hpp>

#include "MgrCap.h"
#include "include/stringify.h"
#include "include/ipaddr.h"
#include "common/debug.h"
#include "common/Formatter.h"

#include <algorithm>
#include <regex>

#include "include/ceph_assert.h"

static inline bool is_not_alnum_space(char c) {
  return !(isalpha(c) || isdigit(c) || (c == '-') || (c == '_'));
}

static std::string maybe_quote_string(const std::string& str) {
  if (find_if(str.begin(), str.end(), is_not_alnum_space) == str.end())
    return str;
  return std::string("\"") + str + std::string("\"");
}

#define dout_subsys ceph_subsys_mgr

std::ostream& operator<<(std::ostream& out, const mgr_rwxa_t& p) {
  if (p == MGR_CAP_ANY)
    return out << "*";

  if (p & MGR_CAP_R)
    out << "r";
  if (p & MGR_CAP_W)
    out << "w";
  if (p & MGR_CAP_X)
    out << "x";
  return out;
}

std::ostream& operator<<(std::ostream& out, const MgrCapGrantConstraint& c) {
  switch (c.match_type) {
  case MgrCapGrantConstraint::MATCH_TYPE_EQUAL:
    out << "=";
    break;
  case MgrCapGrantConstraint::MATCH_TYPE_PREFIX:
    out << " prefix ";
    break;
  case MgrCapGrantConstraint::MATCH_TYPE_REGEX:
    out << " regex ";
    break;
  default:
    break;
  }
  out << maybe_quote_string(c.value);
  return out;
}

std::ostream& operator<<(std::ostream& out, const MgrCapGrant& m) {
  if (!m.profile.empty()) {
    out << "profile " << maybe_quote_string(m.profile);
  } else {
    out << "allow";
    if (!m.service.empty()) {
      out << " service " << maybe_quote_string(m.service);
    } else if (!m.module.empty()) {
      out << " module " << maybe_quote_string(m.module);
    } else if (!m.command.empty()) {
      out << " command " << maybe_quote_string(m.command);
    }
  }

  if (!m.arguments.empty()) {
    out << (!m.profile.empty() ? "" : " with");
    for (auto& [key, constraint] : m.arguments) {
      out << " " << maybe_quote_string(key) << constraint;
    }
  }

  if (m.allow != 0) {
    out << " " << m.allow;
  }

  if (m.network.size()) {
    out << " network " << m.network;
  }
  return out;
}

// <magic>
//  fusion lets us easily populate structs via the qi parser.

typedef std::map<std::string, MgrCapGrantConstraint> kvmap;

BOOST_FUSION_ADAPT_STRUCT(MgrCapGrant,
                          (std::string, service)
                          (std::string, module)
                          (std::string, profile)
                          (std::string, command)
                          (kvmap, arguments)
                          (mgr_rwxa_t, allow)
                          (std::string, network))

BOOST_FUSION_ADAPT_STRUCT(MgrCapGrantConstraint,
                          (MgrCapGrantConstraint::MatchType, match_type)
                          (std::string, value))

// </magic>

void MgrCapGrant::parse_network() {
  network_valid = ::parse_network(network.c_str(), &network_parsed,
                                  &network_prefix);
}

void MgrCapGrant::expand_profile(std::ostream *err) const {
  // only generate this list once
  if (!profile_grants.empty()) {
    return;
  }

  if (profile == "read-only") {
    // grants READ-ONLY caps MGR-wide
    profile_grants.push_back({{}, {}, {}, {}, {}, mgr_rwxa_t{MGR_CAP_R}});
    return;
  }

  if (profile == "read-write") {
    // grants READ-WRITE caps MGR-wide
    profile_grants.push_back({{}, {}, {}, {}, {},
                              mgr_rwxa_t{MGR_CAP_R | MGR_CAP_W}});
    return;
  }

  if (profile == "crash") {
    profile_grants.push_back({{}, {}, {}, "crash post", {}, {}});
    return;
  }

  if (profile == "osd") {
    // this is a documented profile (so we need to accept it as valid), but it
    // currently doesn't do anything
    return;
  }

  if (profile == "mds") {
    // this is a documented profile (so we need to accept it as valid), but it
    // currently doesn't do anything
    return;
  }

  if (profile == "rbd" || profile == "rbd-read-only") {
    Arguments filtered_arguments;
    for (auto& [key, constraint] : arguments) {
      if (key == "pool" || key == "namespace") {
        filtered_arguments[key] = std::move(constraint);
      } else {
        if (err != nullptr) {
          *err << "profile '" << profile << "' does not recognize key '" << key
               << "'";
        }
        return;
      }
    }

    mgr_rwxa_t perms = mgr_rwxa_t{MGR_CAP_R};
    if (profile == "rbd") {
      perms = mgr_rwxa_t{MGR_CAP_R | MGR_CAP_W};
    }

    // allow all 'rbd_support' commands (restricted by optional
    // pool/namespace constraints)
    profile_grants.push_back({{}, "rbd_support", {}, {},
                              std::move(filtered_arguments), perms});
    return;
  }

  if (err != nullptr) {
    *err << "unrecognized profile '" << profile << "'";
  }
}

bool MgrCapGrant::validate_arguments(
      const std::map<std::string, std::string>& args) const {
  for (auto& [key, constraint] : arguments) {
    auto q = args.find(key);

    // argument must be present if a constraint exists
    if (q == args.end()) {
      return false;
    }

    switch (constraint.match_type) {
    case MgrCapGrantConstraint::MATCH_TYPE_EQUAL:
      if (constraint.value != q->second)
        return false;
      break;
    case MgrCapGrantConstraint::MATCH_TYPE_PREFIX:
      if (q->second.find(constraint.value) != 0)
        return false;
      break;
    case MgrCapGrantConstraint::MATCH_TYPE_REGEX:
      try {
        std::regex pattern(constraint.value, std::regex::extended);
        if (!std::regex_match(q->second, pattern)) {
          return false;
        }
      } catch(const std::regex_error&) {
        return false;
      }
      break;
    default:
      return false;
    }
  }

  return true;
}

mgr_rwxa_t MgrCapGrant::get_allowed(
    CephContext *cct, EntityName name, const std::string& s,
    const std::string& m, const std::string& c,
    const std::map<std::string, std::string>& args) const {
  if (!profile.empty()) {
    expand_profile(nullptr);
    mgr_rwxa_t a;
    for (auto& grant : profile_grants) {
      a = a | grant.get_allowed(cct, name, s, m, c, args);
    }
    return a;
  }

  if (!service.empty()) {
    if (service != s) {
      return mgr_rwxa_t{};
    }
    return allow;
  }

  if (!module.empty()) {
    if (module != m) {
      return mgr_rwxa_t{};
    }

    // don't test module arguments when validating a specific command
    if (c.empty() && !validate_arguments(args)) {
      return mgr_rwxa_t{};
    }
    return allow;
  }

  if (!command.empty()) {
    if (command != c) {
      return mgr_rwxa_t{};
    }
    if (!validate_arguments(args)) {
      return mgr_rwxa_t{};
    }
    return mgr_rwxa_t{MGR_CAP_ANY};
  }

  return allow;
}

std::ostream& operator<<(std::ostream&out, const MgrCap& m) {
  bool first = true;
  for (auto& grant : m.grants) {
    if (!first) {
      out << ", ";
    }
    first = false;

    out << grant;
  }
  return out;
}

bool MgrCap::is_allow_all() const {
  for (auto& grant : grants) {
    if (grant.is_allow_all()) {
      return true;
    }
  }
  return false;
}

void MgrCap::set_allow_all() {
  grants.clear();
  grants.push_back({{}, {}, {}, {}, {}, mgr_rwxa_t{MGR_CAP_ANY}});
  text = "allow *";
}

bool MgrCap::is_capable(
    CephContext *cct,
    EntityName name,
    const std::string& service,
    const std::string& module,
    const std::string& command,
    const std::map<std::string, std::string>& command_args,
    bool op_may_read, bool op_may_write, bool op_may_exec,
    const entity_addr_t& addr) const {
  if (cct) {
    ldout(cct, 20) << "is_capable service=" << service << " "
                   << "module=" << module << " "
                   << "command=" << command
                   << (op_may_read ? " read":"")
                   << (op_may_write ? " write":"")
                   << (op_may_exec ? " exec":"")
                   << " addr " << addr
                   << " on cap " << *this
                   << dendl;
  }

  mgr_rwxa_t allow;
  for (auto& grant : grants) {
    if (cct)
      ldout(cct, 20) << " allow so far " << allow << ", doing grant " << grant
                     << dendl;

    if (grant.network.size() &&
        (!grant.network_valid ||
         !network_contains(grant.network_parsed,
                           grant.network_prefix,
                           addr))) {
      continue;
    }

    if (grant.is_allow_all()) {
      if (cct) {
        ldout(cct, 20) << " allow all" << dendl;
      }
      return true;
    }

    // check enumerated caps
    allow = allow | grant.get_allowed(cct, name, service, module, command,
                                      command_args);
    if ((!op_may_read || (allow & MGR_CAP_R)) &&
        (!op_may_write || (allow & MGR_CAP_W)) &&
        (!op_may_exec || (allow & MGR_CAP_X))) {
      if (cct) {
        ldout(cct, 20) << " match" << dendl;
      }
      return true;
    }
  }
  return false;
}

void MgrCap::encode(ceph::buffer::list& bl) const {
  // remain backwards compatible w/ MgrCap
  ENCODE_START(4, 4, bl);
  encode(text, bl);
  ENCODE_FINISH(bl);
}

void MgrCap::decode(ceph::buffer::list::const_iterator& bl) {
  // remain backwards compatible w/ MgrCap
  std::string s;
  DECODE_START(4, bl);
  decode(s, bl);
  DECODE_FINISH(bl);
  parse(s, NULL);
}

void MgrCap::dump(ceph::Formatter *f) const {
  f->dump_string("text", text);
}

void MgrCap::generate_test_instances(std::list<MgrCap*>& ls) {
  ls.push_back(new MgrCap);
  ls.push_back(new MgrCap);
  ls.back()->parse("allow *");
  ls.push_back(new MgrCap);
  ls.back()->parse("allow rwx");
  ls.push_back(new MgrCap);
  ls.back()->parse("allow service foo x");
  ls.push_back(new MgrCap);
  ls.back()->parse("allow command bar x");
  ls.push_back(new MgrCap);
  ls.back()->parse("allow service foo r, allow command bar x");
  ls.push_back(new MgrCap);
  ls.back()->parse("allow command bar with k1=v1 x");
  ls.push_back(new MgrCap);
  ls.back()->parse("allow command bar with k1=v1 k2=v2 x");
  ls.push_back(new MgrCap);
  ls.back()->parse("allow module bar with k1=v1 k2=v2 x");
  ls.push_back(new MgrCap);
  ls.back()->parse("profile rbd pool=rbd");
}

// grammar
namespace qi = boost::spirit::qi;
namespace ascii = boost::spirit::ascii;
namespace phoenix = boost::phoenix;

template <typename Iterator>
struct MgrCapParser : qi::grammar<Iterator, MgrCap()> {
  MgrCapParser() : MgrCapParser::base_type(mgrcap) {
    using qi::char_;
    using qi::int_;
    using qi::ulong_long;
    using qi::lexeme;
    using qi::alnum;
    using qi::_val;
    using qi::_1;
    using qi::_2;
    using qi::_3;
    using qi::eps;
    using qi::lit;

    quoted_string %=
      lexeme['"' >> +(char_ - '"') >> '"'] |
      lexeme['\'' >> +(char_ - '\'') >> '\''];
    unquoted_word %= +char_("a-zA-Z0-9_./-");
    str %= quoted_string | unquoted_word;
    network_str %= +char_("/.:a-fA-F0-9][");

    spaces = +(lit(' ') | lit('\n') | lit('\t'));

    // key <=|prefix|regex> value[ ...]
    str_match = -spaces >> lit('=') >> -spaces >>
                qi::attr(MgrCapGrantConstraint::MATCH_TYPE_EQUAL) >> str;
    str_prefix = spaces >> lit("prefix") >> spaces >>
                 qi::attr(MgrCapGrantConstraint::MATCH_TYPE_PREFIX) >> str;
    str_regex = spaces >> lit("regex") >> spaces >>
                 qi::attr(MgrCapGrantConstraint::MATCH_TYPE_REGEX) >> str;
    kv_pair = str >> (str_match | str_prefix | str_regex);
    kv_map %= kv_pair >> *(spaces >> kv_pair);

    // command := command[=]cmd [k1=v1 k2=v2 ...]
    command_match = -spaces >> lit("allow") >> spaces >> lit("command") >> (lit('=') | spaces)
                            >> qi::attr(std::string())
                            >> qi::attr(std::string())
                            >> qi::attr(std::string())
                            >> str
                            >> -(spaces >> lit("with") >> spaces >> kv_map)
                            >> qi::attr(0)
                            >> -(spaces >> lit("network") >> spaces >> network_str);

    // service foo rwxa
    service_match %= -spaces >> lit("allow") >> spaces >> lit("service") >> (lit('=') | spaces)
                             >> str
                             >> qi::attr(std::string())
                             >> qi::attr(std::string())
                             >> qi::attr(std::string())
                             >> qi::attr(std::map<std::string, MgrCapGrantConstraint>())
                             >> spaces >> rwxa
                             >> -(spaces >> lit("network") >> spaces >> network_str);

    // module foo rwxa
    module_match %= -spaces >> lit("allow") >> spaces >> lit("module") >> (lit('=') | spaces)
                            >> qi::attr(std::string())
                            >> str
                            >> qi::attr(std::string())
                            >> qi::attr(std::string())
                            >> -(spaces >> lit("with") >> spaces >> kv_map)
                            >> spaces >> rwxa
                            >> -(spaces >> lit("network") >> spaces >> network_str);

    // profile foo
    profile_match %= -spaces >> -(lit("allow") >> spaces)
                             >> lit("profile") >> (lit('=') | spaces)
                             >> qi::attr(std::string())
                             >> qi::attr(std::string())
                             >> str
                             >> qi::attr(std::string())
                             >> -(spaces >> kv_map)
                             >> qi::attr(0)
                             >> -(spaces >> lit("network") >> spaces >> network_str);

    // rwxa
    rwxa_match %= -spaces >> lit("allow") >> spaces
                          >> qi::attr(std::string())
                          >> qi::attr(std::string())
                          >> qi::attr(std::string())
                          >> qi::attr(std::string())
                          >> qi::attr(std::map<std::string,MgrCapGrantConstraint>())
                          >> rwxa
                          >> -(spaces >> lit("network") >> spaces >> network_str);

    // rwxa := * | [r][w][x]
    rwxa =
      (lit("*")[_val = MGR_CAP_ANY]) |
      (lit("all")[_val = MGR_CAP_ANY]) |
      ( eps[_val = 0] >>
        ( lit('r')[_val |= MGR_CAP_R] ||
          lit('w')[_val |= MGR_CAP_W] ||
          lit('x')[_val |= MGR_CAP_X]
          )
        );

    // grant := allow ...
    grant = -spaces >> (rwxa_match | profile_match | service_match |
                        module_match | command_match) >> -spaces;

    // mgrcap := grant [grant ...]
    grants %= (grant % (*lit(' ') >> (lit(';') | lit(',')) >> *lit(' ')));
    mgrcap = grants  [_val = phoenix::construct<MgrCap>(_1)];
  }

  qi::rule<Iterator> spaces;
  qi::rule<Iterator, unsigned()> rwxa;
  qi::rule<Iterator, std::string()> quoted_string;
  qi::rule<Iterator, std::string()> unquoted_word;
  qi::rule<Iterator, std::string()> str, network_str;

  qi::rule<Iterator, MgrCapGrantConstraint()> str_match, str_prefix, str_regex;
  qi::rule<Iterator, std::pair<std::string, MgrCapGrantConstraint>()> kv_pair;
  qi::rule<Iterator, std::map<std::string, MgrCapGrantConstraint>()> kv_map;

  qi::rule<Iterator, MgrCapGrant()> rwxa_match;
  qi::rule<Iterator, MgrCapGrant()> command_match;
  qi::rule<Iterator, MgrCapGrant()> service_match;
  qi::rule<Iterator, MgrCapGrant()> module_match;
  qi::rule<Iterator, MgrCapGrant()> profile_match;
  qi::rule<Iterator, MgrCapGrant()> grant;
  qi::rule<Iterator, std::vector<MgrCapGrant>()> grants;
  qi::rule<Iterator, MgrCap()> mgrcap;
};

bool MgrCap::parse(const std::string& str, std::ostream *err) {
  auto iter = str.begin();
  auto end = str.end();

  MgrCapParser<std::string::const_iterator> exp;
  bool r = qi::parse(iter, end, exp, *this);
  if (r && iter == end) {
    text = str;

    std::stringstream profile_err;
    for (auto& g : grants) {
      g.parse_network();

      if (!g.profile.empty()) {
        g.expand_profile(&profile_err);
      }
    }

    if (!profile_err.str().empty()) {
      if (err != nullptr) {
        *err << "mgr capability parse failed during profile evaluation: "
             << profile_err.str();
      }
      return false;
    }
    return true;
  }

  // Make sure no grants are kept after parsing failed!
  grants.clear();

  if (err) {
    if (iter != end)
      *err << "mgr capability parse failed, stopped at '"
           << std::string(iter, end) << "' of '" << str << "'";
    else
      *err << "mgr capability parse failed, stopped at end of '" << str << "'";
  }

  return false;
}
