bundle common user_tests
{
  vars:
    !windows::
      "group1" string => "bin";
      "group2" string => "sys";
      "gid2" string => "3";
    redhat|suse.!suse_15::
      "gid1" string => "1";
    !redhat.(!suse|suse_15).!windows::
      "gid1" string => "2";

    windows::
      "group1" string => "Users";
      "group2" string => "Administrators";

    freebsd::
      "pw_path" string => "/usr/sbin/pw";
      "sudo_path" string => "/usr/local/bin/sudo";
    !freebsd::
      "pw_path" string => "";
      "sudo_path" string => "/usr/bin/sudo";

  classes:
    # On HPUX we use a sudo hack, which doesn't support '-u'.
    !windows.!hpux::
      "sudo_works" expression => "any";

    hpux::
      "passwords_in_shadow" expression => fileexists("/etc/shadow");
      "passwords_in_passwd" not => fileexists("/etc/shadow");
    freebsd::
      "passwords_in_shadow" expression => "!any";
      "passwords_in_passwd" expression => "!any";
      "passwords_in_master_passwd" expression => "any";
    windows|aix::
      "passwords_in_shadow" expression => "!any";
      "passwords_in_passwd" expression => "!any";
    !hpux.!windows.!aix.!freebsd::
      "passwords_in_shadow" expression => "any";
      "passwords_in_passwd" expression => "!any";
    windows|aix::
      "passwords_in_passwd_format" expression => "!any";
    !windows.!aix::
      "passwords_in_passwd_format" expression => "any";
}

bundle agent remove_stale_groups
{
  # When we create a user and change its primary GID afterwards, its original
  # group may linger and cause problems later.
  commands:
    aix::
      "rmgroup johndoe"
        contain => in_shell;
    !aix::
      "$(user_tests.pw_path) groupdel johndoe"
        contain => in_shell;
}

bundle agent user_exists(user, true_class, false_class)
{
  commands:
    !windows::
      "$(G.grep) '^$(user):' /etc/passwd"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");
  vars:
    windows::
      "output" string => execresult("net user $(user)", "useshell");
      "reg" string => escape("$(user)");
  classes:
    windows::
      "$(true_class)" expression => regcmp(".*$(reg).*", "$(output)"),
        scope => "namespace";
      "$(false_class)" not => regcmp(".*$(reg).*", "$(output)"),
        scope => "namespace";
}

bundle agent user_has_uid(user, uid, true_class, false_class)
{
  commands:
    !windows::
      "$(G.grep) '^$(user):[^:]*:$(uid):' /etc/passwd"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  reports:
    windows::
      "Cannot check uid on Windows!";
}

bundle agent user_is_in_primary_group(user, group, true_class, false_class)
{
  vars:
    !windows::
      "no" int => getfields("$(group):.*", "/etc/group", ":", "gid_number");

  commands:
    !windows::
      "$(G.grep) '^$(user):[^:]*:[^:]*:$(gid_number[3]):' /etc/passwd"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  methods:
    windows::
      "redirect" usebundle => user_is_in_secondary_group("$(user)", "$(group)", "$(true_class)", "$(false_class)");
}

bundle agent user_is_in_secondary_group(user, group, true_class, false_class)
{
  commands:
    !windows::
      "$(G.egrep) '^$(group):[^:]*:[^:]*:[^:]*,?$(user)(,|$)' /etc/group"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  vars:
    windows::
      "output" string => execresult("net user $(user)", "useshell");
      "reg" string => escape("$(group)");
  classes:
    windows::
      "$(true_class)" expression => regcmp(".*Local Group Memberships *(\*[a-zA-Z0-9]* *)* *$(reg).*", "$(output)"),
        scope => "namespace";
      "$(false_class)" not => regcmp(".*Local Group Memberships *(\*[a-zA-Z0-9]* *)* *$(reg).*", "$(output)"),
        scope => "namespace";
}

bundle agent user_is_in_any_secondary_group(user, true_class, false_class)
{
  commands:
    !windows::
      "$(G.egrep) '^[^:]*:[^:]*:[^:]*:[^:]*,?$(user)(,|$)' /etc/group"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  vars:
    windows::
      "output" string => execresult("net user $(user)", "useshell");
  classes:
    windows::
      "$(true_class)" expression => regcmp(".*Local Group Memberships *\*None.*", "$(output)"),
        scope => "namespace";
      "$(false_class)" not => regcmp(".*Local Group Memberships *\*None.*", "$(output)"),
        scope => "namespace";
}

bundle agent user_has_home_dir(user, home_dir, true_class, false_class)
{
  commands:
    !windows::
      "$(G.grep) '^$(user):[^:]*:[^:]*:[^:]*:[^:]*:$(home_dir):' /etc/passwd"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  vars:
    windows::
      "output" string => execresult("net user $(user)", "useshell");
      "reg" string => escape("$(home_dir)");
  classes:
    windows::
      "$(true_class)" expression => regcmp(".*Home directory *$(reg).*", "$(output)"),
        scope => "namespace";
      "$(false_class)" not => regcmp(".*Home directory *$(reg).*", "$(output)"),
        scope => "namespace";
}

bundle agent user_has_shell(user, shell, true_class, false_class)
{
  commands:
    !windows::
      "$(G.grep) '^$(user):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:$(shell)$' /etc/passwd"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  reports:
    windows::
      "Cannot check shell on Windows!";
}

bundle agent user_has_description(user, description, true_class, false_class)
{
  commands:
    !windows::
      "$(G.grep) '^$(user):[^:]*:[^:]*:[^:]*:$(description):' /etc/passwd"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  vars:
    windows::
      "output" string => execresult("net user $(user)", "useshell");
  classes:
    windows::
      "$(true_class)" expression => regcmp(".*Full Name *$(description).*", "$(output)"),
        scope => "namespace";
      "$(false_class)" not => regcmp(".*Full Name *$(description).*", "$(output)"),
        scope => "namespace";
}

bundle agent user_has_password(user, password, true_class, false_class)
{
  reports:
    !windows::
      "Cannot test password on Unix!";

  classes:
    windows.has_psexec::
      "$(true_class)" expression => returnszero("$(G.psexec) -u $(user) -p $(password) $(sys.cf_agent) -h", "noshell"),
        scope => "namespace";
      "$(false_class)" not => returnszero("$(G.psexec) -u $(user) -p $(password) $(sys.cf_agent) -h", "noshell"),
        scope => "namespace";
  reports:
    windows.!has_psexec::
      "Need PsExec.exe tool from PSTools to test password!";
}

bundle agent user_has_password_hash(user, hash, true_class, false_class)
{
  vars:
    !aix::
      "escaped_hash" string => escape("$(hash)");
    aix::
      "user_hash" string => execresult("$(this.promise_dirname)/aix_get_shadow_field.pl password $(user)", "useshell");
    passwords_in_passwd::
      "passwd_file" string => "/etc/passwd";
    passwords_in_shadow::
      "passwd_file" string => "/etc/shadow";
    passwords_in_master_passwd::
      "passwd_file" string => "/etc/master.passwd";

  classes:
    aix::
      "$(true_class)" expression => strcmp($(hash), $(user_hash)),
        scope => "namespace";
      "$(false_class)" not => strcmp($(hash), $(user_hash)),
        scope => "namespace";

  commands:
    passwords_in_passwd_format::
      "$(G.grep) '^$(user):$(escaped_hash):' $(passwd_file)"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  reports:
    windows::
      "Cannot test password hash on Windows!";
}

bundle agent user_get_password_hash(user)
{
  vars:
    passwords_in_passwd::
      "passwd_file" string => "/etc/passwd";
    passwords_in_shadow::
      "passwd_file" string => "/etc/shadow";
    passwords_in_passwd_format::
      "hash" string => execresult("$(G.grep) '^$(user):' $(passwd_file) | $(G.sed) -e 's/[^:]*:\([^:]*\):.*/\1/'", "useshell");
    aix::
      "hash" string => execresult("$(this.promise_dirname)/aix_get_shadow_field.pl password $(user)", "useshell");

  reports:
    windows::
      "Cannot get password hash on Windows!";
}

bundle agent user_is_locked(user, true_class, false_class)
{
  vars:
    solaris::
      # Solaris doesn't support expiry date properly (see users promise code).
      "expiry_date" string => "";
    !solaris::
      # Expiry date should be something non-empty.
      "expiry_date" string => "[^:]";
    aix::
      "user_hash" string => execresult("$(this.promise_dirname)/aix_get_shadow_field.pl password $(user)", "useshell");

  classes:
    aix::
      "$(true_class)" expression => regcmp("!.*", $(user_hash)),
        scope => "namespace";
      "$(false_class)" not => regcmp("!.*", $(user_hash)),
        scope => "namespace";

  commands:
    passwords_in_shadow::
      # Notice the [^:] without * at the end. That field (expiry) should be *something*.
      "$(G.grep) '^$(user):![^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:$(expiry_date)[^:]*:' /etc/shadow"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");
    passwords_in_passwd::
      "$(G.grep) '^$(user):![^:]*:' /etc/passwd"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  vars:
    windows::
      "output" string => execresult("net user $(user)", "useshell");
  classes:
    windows::
      "$(true_class)" expression => regcmp(".*Account active *No.*", "$(output)"),
        scope => "namespace";
      "$(false_class)" not => regcmp(".*Account active *No.*", "$(output)"),
        scope => "namespace";
}

bundle agent user_is_unlocked(user, true_class, false_class)
{
  vars:
    aix::
      "user_hash" string => execresult("$(this.promise_dirname)/aix_get_shadow_field.pl password $(user)", "useshell");

  classes:
    aix::
      "$(true_class)" not => regcmp("!.*", $(user_hash)),
        scope => "namespace";
      "$(false_class)" expression => regcmp("!.*", $(user_hash)),
        scope => "namespace";

  commands:
    passwords_in_shadow::
      # Notice the field at the end. That field (expiry) should be gone or zero.
      "$(G.egrep) '^$(user):[^:!]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:(:|0:)' /etc/shadow"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");
    passwords_in_passwd::
      "$(G.grep) '^$(user):[^:!]*:' /etc/passwd"
        contain => no_output_shell,
        classes => on_success("$(true_class)", "$(false_class)");

  vars:
    windows::
      "output" string => execresult("net user $(user)", "useshell");
  classes:
    windows::
      "$(true_class)" expression => regcmp(".*Account active *Yes.*", "$(output)"),
        scope => "namespace";
      "$(false_class)" not => regcmp(".*Account active *Yes.*", "$(output)"),
        scope => "namespace";
}

# Some platforms add a flag that forces the user to change the password.
bundle agent user_does_not_need_password_update(user, true_class, false_class)
{
  vars:
    aix::
      "flags" string => execresult("$(this.promise_dirname)/aix_get_shadow_field.pl flags $(user)", "useshell");

  classes:
    aix::
      "$(true_class)" not => regcmp(".*ADMCHG.*", "$(flags)"),
        scope => "namespace";
      "$(false_class)" expression => regcmp(".*ADMCHG.*", "$(flags)"),
        scope => "namespace";

    !aix::
      "$(true_class)" expression => "any",
        scope => "namespace";
}

body classes on_success(true_class, false_class)
{
  promise_repaired => { "$(true_class)" };
  repair_failed    => { "$(false_class)" };
  cancel_notkept   => { "$(true_class)" };
  cancel_repaired  => { "$(false_class)" };
}

body contain no_output_shell
{
  no_output => "true";
  useshell => "useshell";
}
