module Nexpose

  # Constants
  module Privilege

    module Global
      CREATE_REPORTS              = 'CreateReports'
      CONFIGURE_GLOBAL_SETTINGS   = 'ConfigureGlobalSettings'
      MANAGE_SITES                = 'ManageSites'
      MANAGE_ASSET_GROUPS         = 'ManageAssetGroups'
      MANAGE_DYNAMIC_ASSET_GROUPS = 'ManageDynamicAssetGroups'
      MANAGE_SCAN_TEMPLATES       = 'ManageScanTemplates'
      MANAGE_REPORT_TEMPLATES     = 'ManageReportTemplates'
      GENERATE_RESTRICTED_REPORTS = 'GenerateRestrictedReports'
      MANAGE_SCAN_ENGINES         = 'ManageScanEngines'
      SUBMIT_VULN_EXCEPTIONS      = 'SubmitVulnExceptions'
      APPROVE_VULN_EXCEPTIONS     = 'ApproveVulnExceptions'
      DELETE_VULN_EXCEPTIONS      = 'DeleteVulnExceptions'
      CREATE_TICKETS              = 'CreateTickets'
      CLOSE_TICKETS               = 'CloseTickets'
      TICKET_ASSIGNEE             = 'TicketAssignee'
      ADD_USERS_TO_SITE           = 'AddUsersToSite'
      ADD_USERS_TO_GROUP          = 'AddUsersToGroup'
      ADD_USERS_TO_REPORT         = 'AddUsersToReport'
      MANAGE_POLICIES             = 'ManagePolicies'
      MANAGE_TAGS                 = 'ManageTags'
      MANAGE_ADVPOLICIES          = 'ManageAdvPolicies'
    end

    module Site
      VIEW_ASSET_DATA          = 'ViewAssetData' # NOTE Duplicated between Site and AssetGroup
      CONFIGURE_ALERTS         = 'ConfigureAlerts'
      CONFIGURE_CREDENTIALS    = 'ConfigureCredentials'
      CONFIGURE_ENGINES        = 'ConfigureEngines'
      CONFIGURE_SCAN_TEMPLATES = 'ConfigureScanTemplates'
      CONFIGURE_SCHEDULE_SCANS = 'ConfigureScheduleScans'
      CONFIGURE_SITE_SETTINGS  = 'ConfigureSiteSettings'
      CONFIGURE_TARGETS        = 'ConfigureTargets'
      MANUAL_SCANS             = 'ManualScans'
      PURGE_DATA               = 'PurgeData'
    end

    module AssetGroup
      CONFIGURE_ASSETS = 'ConfigureAssets'
      VIEW_ASSET_DATA  = 'ViewAssetData' # NOTE Duplicated between Site and AssetGroup
    end
  end

  class Connection
    include XMLUtils

    # Returns a summary list of all roles.
    #
    def role_listing
      xml   = make_xml('RoleListingRequest')
      r     = execute(xml, '1.2')
      roles = []
      if r.success
        r.res.elements.each('RoleListingResponse/RoleSummary') do |summary|
          roles << RoleSummary.parse(summary)
        end
      end
      roles
    end

    alias roles role_listing

    def role_delete(role, scope = Scope::SILO)
      xml = make_xml('RoleDeleteRequest')
      xml.add_element('Role', { 'name' => role, 'scope' => scope })
      response = execute(xml, '1.2')
      response.success
    end

    alias delete_role role_delete
  end

  # Role summary object encapsulating information about a role.
  #
  class RoleSummary

    # The short name of the role. Must be unique.
    attr_accessor :name

    # The full name of the role. Must be unique.
    attr_accessor :full_name

    # The unique identifier of the role.
    attr_accessor :id

    # A description of the role.
    attr_accessor :description

    # Whether or not the role is enabled.
    attr_accessor :enabled

    # Specifies if the role has global or silo scope.
    # @see Nexpose::Scope
    attr_accessor :scope

    def initialize(name, full_name, id, description, enabled = true, scope = Scope::SILO)
      @name        = name
      @full_name   = full_name
      @id          = id.to_i
      @description = description
      @enabled     = enabled
      @scope       = scope
    end

    def self.parse(xml)
      new(xml.attributes['name'],
          xml.attributes['full-name'],
          xml.attributes['id'].to_i,
          xml.attributes['description'],
          xml.attributes['enabled'] == 'true',
          xml.attributes['scope'])
    end
  end

  class Role < RoleSummary
    include Sanitize

    # Constants, mapping UI terms to role names expected by API.

    GLOBAL_ADMINISTRATOR  = 'global-admin'
    ASSET_OWNER           = 'system-admin'
    CONTROLS_INSIGHT_ONLY = 'controls-insight-only'
    SECURITY_MANAGER      = 'security-manager'
    SITE_OWNER            = 'site-admin'
    USER                  = 'user'

    # Array of all privileges which are enabled for this role.
    # Note: Although the underlying XML has different requirements, this only checks for presence.
    # @see Nexpose::Privilege
    attr_accessor :privileges

    # Flag to track whether this role exists already on the Nexpose console.
    # Flag determines behavior of #save method.
    attr_accessor :existing

    def initialize(name, full_name, id = -1, enabled = true, scope = Scope::SILO)
      @name       = name
      @full_name  = full_name
      @id         = id.to_i
      @enabled    = enabled
      @scope      = scope
      @privileges = []
    end

    # Retrieve a detailed description of a single role.
    #
    # @param [Connection] nsc Nexpose connection.
    # @param [String] name The short name of the role.
    # @param [String] scope Whether the role has global or silo scope. @see Nexpose::Scope
    #   Scope doesn't appear to be required when requesting installed roles.
    # @return [Role] requested role.
    #
    def self.load(nsc, name, scope = Scope::SILO)
      xml = nsc.make_xml('RoleDetailsRequest')
      xml.add_element('Role', { 'name' => name, 'scope' => scope })
      response = APIRequest.execute(nsc.url, xml, '1.2', { timeout: nsc.timeout, open_timeout: nsc.open_timeout })

      if response.success
        elem = REXML::XPath.first(response.res, 'RoleDetailsResponse/Role/')
        parse(elem)
      end
    end

    alias get load

    # Create or save a Role to the Nexpose console.
    #
    # @param [Connection] nsc Nexpose connection.
    #
    def save(nsc)
      if @existing
        xml = nsc.make_xml('RoleUpdateRequest')
      else
        xml = nsc.make_xml('RoleCreateRequest')
      end
      xml.add_element(as_xml)

      response  = APIRequest.execute(nsc.url, xml, '1.2', { timeout: nsc.timeout, open_timeout: nsc.open_timeout })
      xml       = REXML::XPath.first(response.res, 'RoleCreateResponse')
      @id       = xml.attributes['id'].to_i unless @existing
      @existing = true
      response.success
    end

    # Copy an existing Role to build a new role off of it.
    # Role will not have a valid name or full_name, so they will need to be provided before saving.
    #
    # @param [Connection] nsc Nexpose connection.
    # @param [String] name The short name of the role which you wish to copy.
    # @param [String] scope Whether the role has global or silo scope. @see Nexpose::Scope
    # @return [Role] requested role.
    #
    def self.copy(nsc, name, scope = Scope::SILO)
      role          = load(nsc, name, scope)
      role.name     = role.full_name = nil
      role.id       = -1
      role.existing = false
      role
    end

    # Remove this role from the Nexpose console.
    #
    # @param [Connection] nsc Nexpose connection.
    #
    def delete(nsc)
      nsc.role_delete(name, scope)
    end

    def self.parse(xml)
      role = new(xml.attributes['name'],
                 xml.attributes['full-name'],
                 xml.attributes['id'].to_i,
                 xml.attributes['enabled'] == 'true',
                 xml.attributes['scope'])

      role.description = REXML::XPath.first(xml, 'Description').text
      role.existing = true

      # Only grab enabled privileges.
      xml.elements.each("GlobalPrivileges/child::*[@enabled='true']") do |privilege|
        role.privileges << privilege.name
      end
      xml.elements.each("SitePrivileges/child::*[@enabled='true']") do |privilege|
        role.privileges << privilege.name
      end
      xml.elements.each("AssetGroupPrivileges/child::*[@enabled='true']") do |privilege|
        role.privileges << privilege.name
      end
      role
    end

    def to_xml
      as_xml.to_s
    end

    def as_xml
      xml = REXML::Element.new('Role')
      xml.add_attributes({ 'name' => @name, 'full-name' => @full_name, 'enabled' => enabled, 'scope' => @scope })
      xml.add_attribute('id', @id) if @id > 0
      xml.add_element('Description').text = @description

      site_privileges = xml.add_element('SitePrivileges')
      Privilege::Site.constants.each do |field|
        as_s = Privilege::Site.const_get(field)
        enabled = privileges.member? as_s
        site_privileges.add_element(as_s, { 'enabled' => enabled })
      end

      asset_group_privileges = xml.add_element('AssetGroupPrivileges')
      Privilege::AssetGroup.constants.each do |field|
        as_s = Privilege::AssetGroup.const_get(field)
        enabled = privileges.member? as_s
        asset_group_privileges.add_element(as_s, { 'enabled' => enabled })
      end

      global_privileges = xml.add_element('GlobalPrivileges')
      Privilege::Global.constants.each do |field|
        as_s = Privilege::Global.const_get(field)
        enabled = privileges.member? as_s
        global_privileges.add_element(as_s, { 'enabled' => enabled })
      end

      xml
    end
  end
end
