#!/usr/bin/env jruby

require 'java'

# NetDB Java docs:
# http://web.stanford.edu/group/networking/netdb/rmi/classes/

# There are a couple of ways to tell JRuby where to find classes.  The first
# is with the JVM CLASSPATH environmental variable:
#
#   CLASSPATH=/usr/share/java/netdb_rmi.jar:$CLASSPATH ./example.jruby
#
# The other, which we're going to use here, is to require the jar file in
# the program itself:

require '/usr/share/java/netdb_rmi.jar'

# JRuby doesn't support importing stanford.netdb.*, so we have to import the
# classes one by one.

java_import 'stanford.netdb.NetDB_Datastore'
java_import 'stanford.netdb.NetDB'

java_import 'stanford.netdb.Node'
java_import 'stanford.netdb.Domain'
java_import 'stanford.netdb.User'

# The other classes are listed below in case we ever need one of them.
#
#java_import 'stanford.netdb.A_Name'
#java_import 'stanford.netdb.Address_Space'
#java_import 'stanford.netdb.Admin_Team'
#java_import 'stanford.netdb.Alias'
#java_import 'stanford.netdb.Consultant'
#java_import 'stanford.netdb.Custom_Field'
#java_import 'stanford.netdb.Defaults'
#java_import 'stanford.netdb.Department'
#java_import 'stanford.netdb.DHCP_Option'
#java_import 'stanford.netdb.DHCP_Service'
#java_import 'stanford.netdb.DHCP_Setting'
#java_import 'stanford.netdb.Directory_Person'
#java_import 'stanford.netdb.Domain_Name'
#java_import 'stanford.netdb.Group'
#java_import 'stanford.netdb.IP_Address'
#java_import 'stanford.netdb.IP_Pool'
#java_import 'stanford.netdb.Interface'
#java_import 'stanford.netdb.Interface_IP_Address'
#java_import 'stanford.netdb.Interface_Type'
#java_import 'stanford.netdb.Location'
#java_import 'stanford.netdb.Log'
#java_import 'stanford.netdb.Log_Entry'
#java_import 'stanford.netdb.MX'
#java_import 'stanford.netdb.Make'
#java_import 'stanford.netdb.Model'
#java_import 'stanford.netdb.Model_Type'
#java_import 'stanford.netdb.Name'
#java_import 'stanford.netdb.Network'
#java_import 'stanford.netdb.Node_Type'
#java_import 'stanford.netdb.OS'
#java_import 'stanford.netdb.Person'
#java_import 'stanford.netdb.Privilege'
#java_import 'stanford.netdb.State'
#java_import 'stanford.netdb.IP.IPaddress'
#java_import 'stanford.netdb.IP.Prefix'
#
#java_import 'stanford.netdb.Simple_Search_Result'
#java_import 'stanford.netdb.Admin_Team_SS_Result'
#java_import 'stanford.netdb.Domain_SS_Result'
#java_import 'stanford.netdb.Group_SS_Result'
#java_import 'stanford.netdb.Network_SS_Result'
#java_import 'stanford.netdb.Node_SS_Result'
#java_import 'stanford.netdb.User_SS_Result'
#
#java_import 'stanford.netdb.FS_Boolean'
#java_import 'stanford.netdb.Log_Search_Parameters'
#java_import 'stanford.netdb.Full_Search_Parameters'
#java_import 'stanford.netdb.Admin_Team_FS_Parameters'
#java_import 'stanford.netdb.Domain_FS_Parameters'
#java_import 'stanford.netdb.Group_FS_Parameters'
#java_import 'stanford.netdb.Network_FS_Parameters'
#java_import 'stanford.netdb.Node_FS_Parameters'
#java_import 'stanford.netdb.User_FS_Parameters'

$debug = false

# Global variable to keep track if we have connect to NetDB or not.
$ds    = nil

# Development RMI service
RMI_SERVICE_PROD = 'netdb.stanford.edu'
RMI_SERVICE_DEV  = 'netdb-dev.stanford.edu'
$rmi_service = RMI_SERVICE_PROD

# #############################################################

# Ruby exception classes

class UserNotFound < StandardError
end

class DomainNotFound < StandardError
end

class NodeNotFound < StandardError
end

# #############################################################

# Definition: We say that a user "has control of a node" if the user is listed
# as an admin or a user of that node.

# Definition: We say that a user "has control of a domain" if the user is
# in the "Use as name" section OR is an admin of the domain .

RESULTS = {
  UNKNOWN_ERROR:    1,
  NO_USER:          2,
  NO_NODE:          4,
  NO_DOMAIN:        8,
  CONTROLS_NODE:   16,
  CONTROLS_DOMAIN: 32,
}

# #############################################################

def progress(msg)
  if ($debug) then
    puts 'progress: ' + msg
  end
end


# Print the name and NetID of the current NetDB user
def current_user()
  user = $ds.operating_user
  puts "\nThe current NetDB user is #{user.name} (#{user.netID})."
end

def init_connection
  # Create a NetDB datastore object and make it the default datastore
  if ($ds.nil?()) then
    progress("connecting to RMI service at #{$rmi_service}")
    $ds = NetDB_Datastore.new($rmi_service)
    NetDB.default_datastore($ds)
  else
    progress("already connected to RMI service")
  end
end


def get_user_from_name(user_name)
  begin
    user = User.load(user_name)
  rescue java.lang.Exception
    raise UserNotFound.new("error loading #{user_name}")
  else
    return user
  end
end


# If there is no Domain object in NetDB with name <domain_name> or there
# is no User object in NetDB with name <user_name>, raise an exception.
#
# Otherwise, return true if <user_name> has the "USE_DOMAIN_AS_NAME" right
# for domain <domain_name>, false otherwise. The "USE_DOMAIN_AS_NAME"
# right means that the user can create a node object with the same name as
# the domain object.

# Does this user have the right to use this domain as a name (i.e.,
# create a Node with the domain name).
def can_use_domain_as_name(domain, sunetid)
  progress("checking to see if #{sunetid} can use '#{domain.name}' as name")
  d_groups = domain.get_acl(Domain::ACCESS_TYPE::USE_DOMAIN_AS_NAME)

  # Fetch a list of all the groups this user belongs to. Note that there might
  # not be any NetDB User object corresponding to sunetid.
  user = nil
  begin
    user = get_user_from_name(sunetid)
  rescue UserNotFound => exc
    progress("no user #{sunetid} in NetDB")
  end

  if (! user.nil?()) then
    u_groups = user.owners
  else
    # Since no User object this user is not a member of any groups. So, we
    # might as well return false now.
    return false
  end

  intersection = d_groups.map{|g| g.name} & u_groups.map{|g| g.name}

  return !intersection.empty?

end


# Is this user an Admin of the provided domain?
def is_domain_admin(domain, sunetid)
  return is_an_admin(domain, sunetid)

#  sunetid     = user.netID()
#  domain_name = domain.name
#
#  admins = domain.admins
#
#  admins.each do |admin|
#    progress("node #{domain_name} has admin #{admin.name}")
#
#    netID = admin.netID().to_s()
#    progress("node #{domain_name} has an admin user with sunetID #{netID}")
#    if (netID == sunetid) then
#      progress("user #{sunetid} matches node #{domain_name}'s admin user #{netID}")
#      return true
#    end
#  end
#
#  return false
end

def is_instanceOf_Admin_Team(obj)
  return (obj.getClass().to_s() =~ /Admin_Team/)
end

# Return true if user is an Administrator of node, false
# otherwise.
#
# user must be a netdb.User object.
def is_an_admin(object, sunetid)
  progress("checking to see if #{sunetid} is an admin of this object")

  # Get a list of all the admins of this node. Note that this can include
  # both Admin Teams (lists of users) and explicit users.
  n_admins = object.admins()

  if (object.class.name =~ /Node$/) then
    progress("this is a Node object")
    object_name = object.names().firstElement().full_name()
  elsif (object.class.name =~ /Domain$/) then
    progress("this is a Domain object")
    object_name = object.name
  else
    progress("this is an UNKNOWN object")
    raise "HELP"
  end
  progress("the name of this object is #{object_name}")

  admin_teams = []
  n_admins.each do |admin|
    progress("node #{object_name} has admin #{admin.name}")
    if (is_instanceOf_Admin_Team(admin)) then
      # This is an admin_team, so add to the admin_teams array.
      # puts "found an admin team '#{admin.name}'"
      progress("node #{object_name} has admin team #{admin.name}")
      admin_teams.push(admin)
    else
      # If one of the admins is equal to sunetid then we know the user has
      # control of this node.
      netID = admin.netID().to_s()
      progress("node #{object_name} has an admin user #{netID}")
      if (netID == sunetid) then
        progress("user #{sunetid} matches node #{object_name}'s admin user #{netID}")
        return true
      end
    end
  end

  # If we get here we know that the sunetid was not explicitly listed as
  # an admin. Perhaps sunetid is in one of the admin_teams. So, if there
  # are any admin_teams, see if user_name is in one of them.
  if (admin_teams.size() > 0) then
    progress("node #{object_name} has at least one Admin Team")
    admin_teams.each do |admin_team|
      admin_team_name = admin_team.name
      progress("checking to see if #{sunetid} is a member of #{object_name}'s Admin Team #{admin_team_name}")

      # We add the "complete()" method call to make sure that all the
      # data is loaded. Not doing so would result in members always being
      # the empty array.
      members = admin_team.complete().members().to_a()

      # Is sunetid one of the members? If so, we can return true.
      members.each do |member|
        member_netID = member.netID()
        if (sunetid == member_netID) then
          progress("Admin Team member #{member_netID} matches #{sunetid}")
          return true
        else
          progress("Admin Team member #{member_netID} does not match #{sunetid}")
        end
      end
    end
  end

  # If we get here then user_name is not an Administrator, either
  # explicitly listed or as a member of one of the node's Admin Teams.
  progress("user #{sunetid} is NOT an Administrator of #{object_name}")
  return false
end

# Return true if user_name is listed as a User for node, false
# otherwise.
def is_a_user(node, sunetid)
  # Get all the users.
  n_users = node.users()

  node_name = node.names().firstElement().full_name()

  # Is user_name in any of these?
  n_users.each do |user|
    if (sunetid == user.netID())
      progress("node #{node_name} user #{sunetid} matches #{sunetid}")
      return true
    else
      progress("node #{node_name} user #{sunetid} does not match #{sunetid}")
    end
  end

  progress("no user of node #{node_name} matches #{sunetid}")
  return false
end

# Returns true if user has control of node, false otherwise
def has_control_of_node(node_name, sunetid)

  # Find all nodes with name or alias equal to node_name
  progress("searching for nodes with name or alias '#{node_name}'")
  node_matches = Node.search(node_name)

  node = nil
  nodes_found = []

  # match_to_node is a hash mapping a Node.search result to
  # its node.
  match_to_node = {}
  node_matches.each do |node_match|
    node          = Node.load(node_match.node_name())
    cur_name      = node_match.name
    cur_node_name = node_match.node_name
    types         = node_match.types
    node_type     = node_match.name_type
    progress("matched node has name '#{cur_name}' [#{node_type}]")

    # Skip nodes that are not of type "Node", "Alias", "IP Addr", "MX", or "Interface".
    if (node_type.to_s() !~ /node|alias|ip[ ]addr|interface|mx/i) then
      progress("skipping node '#{cur_node_name}' since it not of type 'Node', 'Alias', 'IP Addr.', 'MX', or 'Interface'")
    else
      match_to_node[node_match] = node
      progress("matched node has node name '#{cur_node_name}'")
    end
  end

  # If we get NO matches, return false.
  num_matches = match_to_node.keys.length()
  if (num_matches == 0) then
    raise NodeNotFound.new("No Node object matching '#{node_name}' found")
  else
    progress("found #{num_matches} node(s) matching '#{node_name}'")
    match_to_node.keys.each do |node_match|
      cur_node_name = node_match.node_name
      progress("considering #{sunetid}'s ownership of node '#{cur_node_name}'")
      node_found = match_to_node[node_match]
      if (is_an_admin(node_found, sunetid) || is_a_user(node_found, sunetid)) then
        return true
      else
        progress("#{sunetid} is not an admin or user of '#{cur_node_name}'")
      end
    end

    progress("#{sunetid} is not an admin or user of any node matching '#{node_name}'")
    return false
  end

end

# Returns true if user has control of domain, false otherwise
def has_control_of_domain(domain_name, sunetid)

  progress("searching for domain with name '#{domain_name}'")

  begin
    domain = Domain.load(domain_name)
  rescue java.lang.Exception
    raise DomainNotFound.new("Domain object #{domain_name} not found")
  end

  if (can_use_domain_as_name(domain, sunetid) ||
      is_domain_admin(domain, sunetid)) then
    return true
  else
    return false
  end

end


def get_result(sunetid,
               hostname,
               process_node_info = false,
               process_domain_info = false)

  # Make connection to NetDB
  init_connection()

  # It turns out that there are users listed as administrators of NetDB
  # Nodes that do _not_ have a corresponding NetDB User record. That is,
  # these people are admins of the node but cannot actually access
  # NetDB. This is done so that the appropriate person can be contacted in
  # case of an server issue without giving that person NetDB access.
  #
  # In light of this, we skip checking to see if the user has a NetDB
  # record. (We leave the code in place in case we change our mind later.)
  if false then
    # Make sure user is in NetDB.
    user = nil
    begin
      user = get_user_from_name(sunetid)
    rescue UserNotFound => exc
      progress("no user #{sunetid} in NetDB")
      return RESULTS[:NO_USER]
    end
  end

  result = 0

  if (process_node_info)
    # Does user have control of DOMAIN hostname?
    begin
      progress("checking to see if user #{sunetid} controls node #{hostname}")
      if (has_control_of_node(hostname, sunetid)) then
        result = result + RESULTS[:CONTROLS_NODE]
        progress("user #{sunetid} controls node #{hostname}")
      else
        progress("user #{sunetid} does not control node #{hostname}")
      end
    rescue NodeNotFound => exc
      progress("no Node #{hostname} found in NetDB")
      result += RESULTS[:NO_NODE]
    end
  end

  if (process_domain_info)
    # Does user have control of DOMAIN hostname?
    progress("checking to see if user #{sunetid} controls domain #{hostname}")
    begin
      if (has_control_of_domain(hostname, sunetid)) then
        result = result + RESULTS[:CONTROLS_DOMAIN]
        progress("user #{sunetid} has control of domain #{hostname}")
      else
        progress("user #{sunetid} does not have control of domain #{hostname}")
      end
    rescue DomainNotFound => exc
      progress("no domain #{hostname} in NetDB")
      result += RESULTS[:NO_DOMAIN]
    end
  end

  return result
end


require 'optparse'
usage_string = 'some help'

get_node_info   = false
get_domain_info = false

parser = OptionParser.new do |opts|
  opts.banner = "Usage: netdb_access_info.rb [options]"

  opts.separator ""
  opts.separator "Specific options:"

  opts.on("-n", "--get-node-access-info",
          "return only NODE access information") do
    get_node_info = true;
  end

  opts.on("-d", "--get-domain-access-info",
          "return only DOMAIN access information") do
    get_domain_info = true;
  end

  opts.on("-t", "--use-netdb-dev",
          "use development rather than production NetDB") do
    $rmi_service = RMI_SERVICE_DEV
  end

  opts.on('-v', '--verbose', 'verbose mode') do
    $debug = true
  end

  opts.on('-h', '--help', 'display help') do
    puts opts
    puts usage_string
    exit
  end
end

parser.parse!

# If neither get_node_info nor get_domain_info is true, then this means
# the user wants BOTH sets of information.
if (get_node_info and get_domain_info) then
  raise ArgumentError.new("cannot use both -n and -d")
elsif (!get_node_info and !get_domain_info) then
  get_node_info   = true
  get_domain_info = true
end


# The input should be in this format:
#
#  user1,hostname1:user2,hostname2 etc.
#
# Returns
#  user1,hostname1,RESULT:user2,hostname2,RESULT
#
#  RESULT is sum:
#    1  : an unknown error occured
#    2  : no USER   user in NetDB
#    4  : no NODE   hostname in NetDB
#    8  : no DOMAIN hostname in NetDB
#   16  : user has control of NODE hostname
#   32  : user has control of DOMAIN hostname

input_string = ARGV[0]
progress("input_string is #{input_string}")

# Parse the input string.
# fs is the field separator
fs = ':'
tuples = input_string.split(fs)

# Check tuples for length and badness
tuples.each do |tuple|
  # Split into user and hostname
  components  = tuple.split(',')
  if (components.length != 2) then
    raise ArgumentError.new("wrong number of arguments on input '#{tuple}'")
  end

  user_name = components[0]
  hostname  = components[1]

  # Make sure each user_name and hostname contain only allowed characters
  if (hostname !~ /^[a-z0-9\-\_\.]+$/i) then
    raise ArgumentError.new("hostname '#{hostname}' contains disallowed characters")
  end

  if (user_name !~ /^[a-z0-9]+$/i) then
    raise ArgumentError.new("user_name '#{user_name}' contains disallowed characters")
  end
end

# The results hash
results = {}

tuples.each do |tuple|
  # Split into user and hostname
  components = tuple.split(',')
  sunetid    = components[0]
  hostname   = components[1]
  progress("found tuple (#{sunetid},#{hostname})")

  result = get_result(sunetid,
                      hostname,
                      get_node_info,
                      get_domain_info)

  # Remember this result
  results[tuple] = result
  progress("tuple #{tuple} had result #{result}")
end

# Print out results
output = []
results.each do |key, value|
  output.push("#{key},#{value}")
end
puts output.join(fs)
