text: // Copyright 2016 The Chromium OS Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  // Command line tool to parse data. Data is read from stdin as string or
  // protobuf and returned through stdout in string or protobuf format. The tool
  // is invoked by the authpolicy daemon in a secure sandbox. It is done this way
  // since parsing the output is considered insecure.
  //
  // Usage:
  //   authpolicy_parser <command> <serialized_debug_flags>
  //   For a list of commands see constants.h.
  //   Each command reads additional arguments from stdin. See code for details.
  //
  // Logs to syslog.
  
  #include <time.h>
  
  #include <string>
  #include <vector>
  
  #include <base/at_exit.h>
  #include <base/files/file_util.h>
  #include <base/strings/string_number_conversions.h>
  #include <base/strings/string_split.h>
  #include <base/strings/string_util.h>
  #include <brillo/syslog_logging.h>
  
  #include "authpolicy/authpolicy_flags.h"
  #include "authpolicy/constants.h"
  #include "authpolicy/log_colors.h"
  #include "authpolicy/platform_helper.h"
  #include "authpolicy/policy/preg_policy_encoder.h"
  #include "authpolicy/proto_bindings/active_directory_info.pb.h"
  #include "authpolicy/samba_helper.h"
  #include "bindings/authpolicy_containers.pb.h"
  #include "bindings/chrome_device_policy.pb.h"
  #include "bindings/cloud_policy.pb.h"
  
  namespace em = enterprise_management;
  
  namespace authpolicy {
  
  namespace {
  
  // 'net ads gpo list' tokens.
  const char kGpoToken_Separator[] = "---------------------";
  const char kGpoToken_Name[] = "name";
  const char kGpoToken_Filesyspath[] = "filesyspath";
  const char kGpoToken_VersionUser[] = "version_user";
  const char kGpoToken_VersionMachine[] = "version_machine";
  const char kGpoToken_Options[] = "options";
  
  // 'net ads' tokens.
  const char kToken_NoResults[] = "Got 0 replies";
  const char kToken_KdcServer[] = "KDC server";
  const char kToken_ServerTime[] = "Server time";
  const char kToken_DomainController[] = "Domain Controller";
  const char kToken_Workgroup[] = "Workgroup";
  
  // Length of the klist date/time format (mm/dd/yy HH:MM:SS).
  const int kDateTimeStringLength = 18;
  
  // Various offsets from the beginning of a line of date/time strings in the
  // klist output.
  const size_t kValidFromOffset = 0;
  const size_t kExpiresOffset = 19;
  const size_t kRenewUntilOffset = 11;
  
  // String in klist output that prefixes the renewal lifetime.
  const char kRenewUntil[] = "renew until ";
  
  // Grace time before printing warnings like "TGT not yet valid?" since it's
  // generating a lot of false positives otherwise. The reason could be time
  // discrepancies between client and server.
  const int kTgtWarningGraceTimeSeconds = 300;
  
  struct GpoEntry {
    GpoEntry() { Clear(); }
  
    void Clear() {
      name.clear();
      filesyspath.clear();
      version_user = 0;
      version_machine = 0;
      gp_flags = kGpFlagInvalid;
    }
  
    bool IsValid() const {
      return !name.empty() && !filesyspath.empty() &&
             !(version_user == 0 && version_machine == 0) &&
             gp_flags != kGpFlagInvalid;
    }
  
    bool IsEmpty() const {
      return name.empty() && filesyspath.empty() && version_user == 0 &&
             version_machine == 0 && gp_flags == kGpFlagInvalid;
    }
  
    void Log() const {
      LOG(INFO) << kColorGpo << "  Name:        " << name << kColorReset;
      LOG(INFO) << kColorGpo << "  Version:     " << version_user << " (user) "
                << version_machine << " (machine)" << kColorReset;
      LOG(INFO) << kColorGpo << "  GPFLags:     " << gp_flags << kColorReset;
    }
  
    std::string name;
    std::string filesyspath;
    unsigned int version_user;
    unsigned int version_machine;
    int gp_flags;
  };
  
  void PushGpo(const GpoEntry& gpo,
               PolicyScope scope,
               std::vector<GpoEntry>* gpo_list,
               const protos::DebugFlags& flags) {
    if (gpo.IsEmpty())
      return;
  
    if (!gpo.IsValid() && flags.log_gpo()) {
      LOG(INFO) << kColorGpo << "Ignoring invalid GPO" << kColorReset;
      gpo.Log();
      return;
    }
  
    // Filter out GPOs we don't need. If version_user == 0, there's no user
    // policy stored in that GPO. Similarly, if version_machine == 0, there's no
    // device policy.
    const char* filter_reason = nullptr;
    switch (scope) {
      case PolicyScope::USER:
        if (gpo.version_user == 0)
          filter_reason = "user version is 0";
        else if (gpo.gp_flags & kGpFlagUserDisabled)
          filter_reason = "user disabled flag is set";
        break;
      case PolicyScope::MACHINE:
        if (gpo.version_machine == 0)
          filter_reason = "machine version is 0";
        else if (gpo.gp_flags & kGpFlagMachineDisabled)
          filter_reason = "machine disabled flag is set";
        break;
    }
    if (!filter_reason) {
      gpo_list->push_back(gpo);
    } else if (flags.log_gpo()) {
      LOG(INFO) << kColorGpo << "Filtered out GPO (" << filter_reason << ")"
                << kColorReset;
      gpo.Log();
    }
  }
  
  // Prints |str| to stdout for the caller of this tool. Returns an exit code that
  // indicates success or failure.
  int OutputForCaller(const std::string& str) {
    if (!base::WriteFileDescriptor(STDOUT_FILENO, str.c_str(), str.size())) {
      LOG(ERROR) << "Failed to write output for caller";
      return EXIT_CODE_WRITE_OUTPUT_FAILED;
    }
    return EXIT_CODE_OK;
  }
  
  // Parses the substring starting at offset |offset| of |str| for a date/time
  // formatted mm/dd/yy HH:MM:SS. The time is interpreted as local time. Sets
  // |time| to the number of seconds in the epoch or 0 on error. Returns true on
  // success.
  bool ParseTgtDateTime(const std::string& str, size_t offset, time_t* time) {
    *time = 0;
    if (offset >= str.size())
      return false;
  
    std::string datetime = str.substr(offset, kDateTimeStringLength);
    if (datetime.size() < kDateTimeStringLength)
      return false;
  
    struct tm tm = {};
    if (!strptime(datetime.c_str(), "%m/%d/%y %H:%M:%S", &tm))
      return false;
  
    // Figure out daylight saving time (strptime doesn't set this).
    tm.tm_isdst = -1;
  
    *time = mktime(&tm);
    return true;
  }
  
  // Parses the output of net ads info into a ServerInfo protobuf and prints
  // it to stdout.
  int ParseServerInfo(const std::string& net_out) {
    std::string kdc_ip, server_time_str;
    if (!FindToken(net_out, ':', kToken_KdcServer, &kdc_ip) ||
        !FindToken(net_out, ':', kToken_ServerTime, &server_time_str)) {
      LOG(ERROR) << "Failed to parse server info";
      return EXIT_CODE_FIND_TOKEN_FAILED;
    }
  
    // Parse time. The time format is "Thu, 15 Feb 2018 11:21:26 PST".
    base::Time server_time;
    if (!base::Time::FromString(server_time_str.c_str(), &server_time)) {
      LOG(ERROR) << "Failed to parse server time " << server_time_str;
      return EXIT_CODE_PARSE_INPUT_FAILED;
    }
  
    // Put data into proto.
    protos::ServerInfo server_info;
    server_info.set_kdc_ip(kdc_ip);
    server_info.set_server_time(server_time.ToInternalValue());
  
    std::string server_info_blob;
    if (!server_info.SerializeToString(&server_info_blob)) {
      LOG(ERROR) << "Failed to convert server info proto to string";
      return EXIT_CODE_WRITE_OUTPUT_FAILED;
    }
    return OutputForCaller(server_info_blob);
  }
  
  // Parses the output of net ads search to get the user's account info and prints
  // it to stdout. Prints an empty string in case of no search results.
  int ParseAccountInfo(const std::string& net_out) {
    // Return an empty string, but no error, if no results have been found.
    if (base::StartsWith(net_out, kToken_NoResults, base::CompareCase::SENSITIVE))
      return OutputForCaller("");
  
    // Parse required attributes.
    std::string object_guid;
    std::string sam_account_name;
    std::string common_name;
    if (!FindToken(net_out, ':', kSearchObjectGUID, &object_guid) ||
        !FindToken(net_out, ':', kSearchSAMAccountName, &sam_account_name) ||
        !FindToken(net_out, ':', kSearchCommonName, &common_name)) {
      LOG(ERROR) << "Failed to parse account info";
      return EXIT_CODE_FIND_TOKEN_FAILED;
    }
  
    // Put data into proto.
    ActiveDirectoryAccountInfo account_info;
    account_info.set_account_id(object_guid);
    account_info.set_sam_account_name(sam_account_name);
    account_info.set_common_name(common_name);
  
    // pwdLastSet might be missing, see crbug.com/795758. Handle it gracefully.
    std::string pwd_last_set_str;
    if (FindToken(net_out, ':', kSearchPwdLastSet, &pwd_last_set_str)) {
      uint64_t pwd_last_set;
      if (!base::StringToUint64(pwd_last_set_str, &pwd_last_set)) {
        LOG(WARNING) << "Failed to convert pwdLastSet string '"
                     << pwd_last_set_str << "' to integer";
      } else {
        account_info.set_pwd_last_set(pwd_last_set);
      }
    }
  
    // Likewise, handle missing userAccountControl just in case.
    std::string user_account_control_str;
    if (FindToken(net_out, ':', kSearchUserAccountControl,
                  &user_account_control_str)) {
      uint32_t user_account_control;
      if (!base::StringToUint(user_account_control_str, &user_account_control)) {
        LOG(WARNING) << "Failed to convert userAccountControl string '"
                     << user_account_control_str << "' to integer";
      } else {
        account_info.set_user_account_control(user_account_control);
      }
    }
  
    // Attributes 'displayName' and 'givenName' are optional. May be missing for
    // accounts like 'Administrator' or for partially set up accounts.
    std::string display_name, given_name;
    if (FindToken(net_out, ':', kSearchDisplayName, &display_name))
      account_info.set_display_name(display_name);
    if (FindToken(net_out, ':', kSearchGivenName, &given_name))
      account_info.set_given_name(given_name);
  
    std::string account_info_blob;
    if (!account_info.SerializeToString(&account_info_blob)) {
      LOG(ERROR) << "Failed to convert account info proto to string";
      return EXIT_CODE_WRITE_OUTPUT_FAILED;
    }
    return OutputForCaller(account_info_blob);
  }
  
  // Parses the output of a net ads command for '|token| : value'. Prints value to
  // stdout.
  int ParseSingleToken(const std::string& net_out, const std::string& token) {
    std::string value;
    if (!FindToken(net_out, ':', token, &value))
      return EXIT_CODE_FIND_TOKEN_FAILED;
  
    return OutputForCaller(value);
  }
  
  // Parses the output of net ads gpo list to get the list of GPOs. Prints out a
  // serialized GpoList blob to stdout.
  int ParseGpoList(const std::string& net_out,
                   PolicyScope scope,
                   const protos::DebugFlags& flags) {
    // Parse net output.
    GpoEntry current_gpo;
    std::vector<GpoEntry> gpo_list;
    const std::vector<std::string> lines = base::SplitString(
        net_out, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
    LOG_IF(INFO, flags.log_gpo()) << kColorGpo << "Parsing GPO list ("
                                  << lines.size() << " lines)" << kColorReset;
    bool found_separator = false;
    for (const std::string& line : lines) {
      if (line.find(kGpoToken_Separator) == 0) {
        // Separator between entries. Process last gpo if any.
        PushGpo(current_gpo, scope, &gpo_list, flags);
        current_gpo.Clear();
        found_separator = true;
        continue;
      }
  
      // Collect data
      const size_t colon_pos = line.find(":");
      if (colon_pos == std::string::npos || colon_pos + 1 >= line.size())
        continue;
      const std::string key = line.substr(0, colon_pos);
      std::string value = line.substr(colon_pos + 1);
      base::TrimWhitespaceASCII(value, base::TRIM_ALL, &value);
  
      bool already_set = false;
      bool version_error = false;
      bool flags_error = false;
      if (key == kGpoToken_Name) {
        already_set = !current_gpo.name.empty();
        current_gpo.name = value;
      } else if (key == kGpoToken_Filesyspath) {
        already_set = !current_gpo.filesyspath.empty();
        current_gpo.filesyspath = value;
      } else if (key == kGpoToken_VersionUser) {
        already_set = current_gpo.version_user != 0;
        version_error = !ParseGpoVersion(value, &current_gpo.version_user);
      } else if (key == kGpoToken_VersionMachine) {
        already_set = current_gpo.version_machine != 0;
        version_error = !ParseGpoVersion(value, &current_gpo.version_machine);
      } else if (key == kGpoToken_Options) {
        already_set = current_gpo.gp_flags != kGpFlagInvalid;
        flags_error = !ParseGpFlags(value, &current_gpo.gp_flags);
      }
  
      // Sanity check that we don't miss separators between GPOs.
      if (already_set) {
        LOG(ERROR) << "Failed to parse GPO data (bad format)";
        return EXIT_CODE_PARSE_INPUT_FAILED;
      }
  
      if (version_error) {
        LOG(ERROR) << "Failed to parse GPO version '" << value << "'";
        return EXIT_CODE_PARSE_INPUT_FAILED;
      }
  
      if (flags_error) {
        LOG(ERROR) << "Failed to parse GP flags '" << value << "'";
        return EXIT_CODE_PARSE_INPUT_FAILED;
      }
    }
  
    // Just in case there's no separator in the end.
    PushGpo(current_gpo, scope, &gpo_list, flags);
  
    if (!found_separator) {
      // This usually happens when something went wrong, e.g. connection error.
      LOG(ERROR) << "Failed to parse GPO data (no separator, did net fail?)";
      return EXIT_CODE_PARSE_INPUT_FAILED;
    }
  
    if (flags.log_gpo() && LOG_IS_ON(INFO)) {
      LOG(INFO) << kColorGpo << "Found " << gpo_list.size() << " GPOs."
                << kColorReset;
      for (size_t n = 0; n < gpo_list.size(); ++n) {
        LOG(INFO) << kColorGpo << n + 1 << ")" << kColorReset;
        gpo_list[n].Log();
      }
    }
  
    // Convert to proto.
    protos::GpoList gpo_list_proto;
    for (const GpoEntry& gpo : gpo_list) {
      // Split the filesyspath, e.g.
      //   \\chrome.lan\SysVol\chrome.lan\Policies\{3507856D-...-CF144DC5CC3A}
      // into
      // - the share (SysVol) and
      // - the directory (chrome.lan\Policies\...).
      // The first part (chrome.lan) is dropped and replaced by the domain
      // controller name when the GPOs are downloaded via smbclient.
      const std::vector<std::string> file_parts = base::SplitString(
          gpo.filesyspath, "\\/", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
      if (file_parts.size() < 4 || !file_parts[0].empty() ||
          !file_parts[1].empty()) {
        LOG(ERROR) << "Failed to split filesyspath '" << gpo.filesyspath
                   << "' into service and directory parts";
        return EXIT_CODE_PARSE_INPUT_FAILED;
      }
      const std::string& share = file_parts[3];
      const std::string directory = base::JoinString(
          std::vector<std::string>(file_parts.begin() + 4, file_parts.end()),
          "\\");
  
      protos::GpoEntry* gpo_proto = gpo_list_proto.add_entries();
      gpo_proto->set_name(gpo.name);
      gpo_proto->set_share(share);
      gpo_proto->set_directory(directory);
    }
  
    // Output data as proto blob.
    std::string gpo_list_blob;
    if (!gpo_list_proto.SerializeToString(&gpo_list_blob)) {
      LOG(ERROR) << "Failed to convert GPO list proto to string";
      return EXIT_CODE_WRITE_OUTPUT_FAILED;
    }
    return OutputForCaller(gpo_list_blob);
  }
  
  // Parses a set of GPO files and assembles a user or device policy proto. Writes
  // the serialized policy blob to stdout. |gpo_file_paths_blob| is expected to be
  // a serialized |protos::FilePathList| proto blob.
  int ParsePreg(const std::string& gpo_file_paths_blob,
                PolicyScope scope,
                const protos::DebugFlags& flags) {
    // Parse FilePathList proto blob.
    protos::FilePathList gpo_file_paths_proto;
    if (!gpo_file_paths_proto.ParseFromString(gpo_file_paths_blob)) {
      LOG(ERROR) << "Failed to parse file paths blob";
      return EXIT_CODE_READ_INPUT_FAILED;
    }
  
    // Convert to list of base::FilePaths.
    std::vector<base::FilePath> gpo_file_paths;
    for (int n = 0; n < gpo_file_paths_proto.entries_size(); ++n)
      gpo_file_paths.push_back(base::FilePath(gpo_file_paths_proto.entries(n)));
  
    protos::GpoPolicyData data;
    switch (scope) {
      case PolicyScope::USER: {
        // Parse files into a user policy proto.
        em::CloudPolicySettings policy;
        if (!policy::ParsePRegFilesIntoUserPolicy(gpo_file_paths, &policy,
                                                  flags.log_policy_values())) {
          return EXIT_CODE_PARSE_INPUT_FAILED;
        }
  
        // Serialize user policy proto to string.
        if (!policy.SerializeToString(data.mutable_user_or_device_policy()))
          return EXIT_CODE_WRITE_OUTPUT_FAILED;
        break;
      }
      case PolicyScope::MACHINE: {
        // Parse files into a device policy proto.
        em::ChromeDeviceSettingsProto policy;
        if (!policy::ParsePRegFilesIntoDevicePolicy(gpo_file_paths, &policy,
                                                    flags.log_policy_values())) {
          return EXIT_CODE_PARSE_INPUT_FAILED;
        }
  
        // Serialize policy proto to string.
        if (!policy.SerializeToString(data.mutable_user_or_device_policy()))
          return EXIT_CODE_WRITE_OUTPUT_FAILED;
        break;
      }
      default: { LOG(FATAL) << "invalid scope"; }
    }
  
    // Parse GPOs again for extension policy. Note that it might be contained in
    // both scopes (USER and MACHINE). Note that this is slightly inefficient as
    // it loads and parses each GPO file a second time. It would be better if
    // preg_parser accepted multiple keys.
    policy::ExtensionPolicies extension_policies;
    if (!policy::ParsePRegFilesIntoExtensionPolicy(
            gpo_file_paths, &extension_policies, flags.log_policy_values())) {
      return EXIT_CODE_PARSE_INPUT_FAILED;
    }
    for (protos::ExtensionPolicy& proto : extension_policies)
      *data.add_extension_policies() = std::move(proto);
  
    // Output |data| as serialized string to stdout.
    std::string data_blob;
    if (!data.SerializeToString(&data_blob))
      return EXIT_CODE_WRITE_OUTPUT_FAILED;
    return OutputForCaller(data_blob);
  }
  
  // Parses the validity and renewal lifetimes of a TGT from the output of klist.
  // Writes the serialized lifetime protobuf blob to stdout. For sample klist
  // output see stub_klist_main.cc.
  int ParseTgtLifetime(const std::string& klist_out) {
    std::vector<std::string> lines = base::SplitString(
        klist_out, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  
    time_t valid_from, expires, renew_until = 0;
    for (size_t n = 0; n < lines.size(); ++n) {
      if (Contains(lines[n], "krbtgt/") &&
          ParseTgtDateTime(lines[n], kValidFromOffset, &valid_from) &&
          ParseTgtDateTime(lines[n], kExpiresOffset, &expires)) {
        if (n + 1 < lines.size() &&
            base::StartsWith(lines[n + 1], kRenewUntil,
                             base::CompareCase::SENSITIVE) &&
            ParseTgtDateTime(lines[n + 1], kRenewUntilOffset, &renew_until)) {
          ++n;
        }
  
        // If the caller checked klist -s beforehand, the TGT should be valid and
        // these warnings should never be printed.
        time_t now = time(NULL);
        if (now + kTgtWarningGraceTimeSeconds < valid_from) {
          LOG(WARNING) << "TGT not yet valid? (now=" << now
                       << ", valid_from=" << valid_from << ")";
        }
        if (now + kTgtWarningGraceTimeSeconds > expires) {
          LOG(WARNING) << "TGT already expired? (now=" << now
                       << ", expires=" << expires << ")";
        }
  
        // Output lifetime as protobuf blob.
        protos::TgtLifetime lifetime;
        lifetime.set_validity_seconds(std::max<int64_t>(expires - now, 0));
        lifetime.set_renewal_seconds(std::max<int64_t>(renew_until - now, 0));
  
        std::string lifetime_blob;
        if (!lifetime.SerializeToString(&lifetime_blob)) {
          LOG(ERROR) << "Failed to convert lifetime proto to string";
          return EXIT_CODE_WRITE_OUTPUT_FAILED;
        }
        return OutputForCaller(lifetime_blob);
      }
    }
  
    LOG(ERROR) << "Failed to find krbtgt in klist output";
    return EXIT_CODE_PARSE_INPUT_FAILED;
  }
  
  int HandleCommand(const std::string& cmd,
                    const std::string& arg,
                    const protos::DebugFlags& flags) {
    if (cmd == kCmdParseServerInfo)
      return ParseServerInfo(arg);
    if (cmd == kCmdParseDcName)
      return ParseSingleToken(arg, kToken_DomainController);
    if (cmd == kCmdParseWorkgroup)
      return ParseSingleToken(arg, kToken_Workgroup);
    if (cmd == kCmdParseAccountInfo)
      return ParseAccountInfo(arg);
    if (cmd == kCmdParseUserGpoList)
      return ParseGpoList(arg, PolicyScope::USER, flags);
    if (cmd == kCmdParseDeviceGpoList)
      return ParseGpoList(arg, PolicyScope::MACHINE, flags);
    if (cmd == kCmdParseUserPreg)
      return ParsePreg(arg, PolicyScope::USER, flags);
    if (cmd == kCmdParseDevicePreg)
      return ParsePreg(arg, PolicyScope::MACHINE, flags);
    if (cmd == kCmdParseTgtLifetime)
      return ParseTgtLifetime(arg);
  
    LOG(ERROR) << "Bad command";
    return EXIT_CODE_BAD_COMMAND;
  }
  
  }  // namespace
  
  }  // namespace authpolicy
  
  int main(int argc, char* argv[]) {
    brillo::OpenLog("authpolicy_parser", true);
    brillo::InitLog(brillo::kLogToSyslog);
  
    // Required for base::SysInfo.
    base::AtExitManager at_exit_manager;
  
    // Require one argument, one of the kCmdParse* strings.
    if (argc <= 1) {
      LOG(ERROR) << "No command";
      return authpolicy::EXIT_CODE_BAD_COMMAND;
    }
    const char* cmd = argv[1];
  
    // Load debug flags from argv[2] if present.
    authpolicy::protos::DebugFlags flags;
    if (argc > 2 && !authpolicy::DeserializeFlags(argv[2], &flags)) {
      LOG(ERROR) << "Failed to deserialize flags";
      return authpolicy::EXIT_CODE_BAD_COMMAND;
    }
  
    // All commands take additional arguments via stdin.
    std::string stdin;
    if (!authpolicy::ReadPipeToString(STDIN_FILENO, &stdin)) {
      LOG(ERROR) << "Failed to read stdin";
      return authpolicy::EXIT_CODE_READ_INPUT_FAILED;
    }
  
    return authpolicy::HandleCommand(cmd, stdin, flags);
  }
  