# Copyright 2014-2016, Tresys Technology, LLC
# Copyright 2016, Chris PeBenito <pebenito@ieee.org>
#
# This file is part of SETools.
#
# SETools is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 2.1 of
# the License, or (at your option) any later version.
#
# SETools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with SETools.  If not, see
# <http://www.gnu.org/licenses/>.
#
# pylint: disable=too-many-public-methods

import logging
import warnings
from itertools import chain
from errno import ENOENT

try:
    import selinux
except ImportError:
    pass

from . import qpol

# The libqpol SWIG class is not quite natural for
# Python the policy is repeatedly referenced in the
# function calls, which makes sense for C code
# but not for python code, so each object keeps
# a reference to the policy for internal use.
# This also makes sense since an object would only
# be valid for the policy it comes from.

# Exceptions
from . import exception

# Components
from . import boolcond
from . import bounds
from . import default
from . import mls
from . import objclass
from . import polcap
from . import role
from . import typeattr
from . import user

# Rules
from . import mlsrule
from . import rbacrule
from . import terule

# Constraints
from . import constraint

# In-policy Labeling
from . import fscontext
from . import initsid
from . import netcontext

# Xen
from . import xencontext

from .util import PolicyEnum


class PolicyTarget(PolicyEnum):

    """Enumeration of policy targets."""

    selinux = qpol.QPOL_TARGET_SELINUX
    xen = qpol.QPOL_TARGET_XEN


class HandleUnknown(PolicyEnum):

    """Enumeration of handle unknown settings."""

    deny = qpol.QPOL_DENY_UNKNOWN
    allow = qpol.QPOL_ALLOW_UNKNOWN
    reject = qpol.QPOL_REJECT_UNKNOWN


class SELinuxPolicy(object):

    """The complete SELinux policy."""

    def __init__(self, policyfile=None):
        """
        Parameter:
        policyfile  Path to a policy to open.
        """

        self.log = logging.getLogger(__name__)
        self.policy = None
        self.filename = None

        if policyfile:
            self._load_policy(policyfile)
        else:
            try:
                self._load_running_policy()
            except NameError:
                raise RuntimeError("Loading the running policy requires libselinux Python bindings")

    def __repr__(self):
        return "<SELinuxPolicy(\"{0}\")>".format(self.filename)

    def __str__(self):
        return self.filename

    def __deepcopy__(self, memo):
        # shallow copy as all of the members are immutable
        newobj = SELinuxPolicy.__new__(SELinuxPolicy)
        newobj.policy = self.policy
        newobj.filename = self.filename
        memo[id(self)] = newobj
        return newobj

    #
    # Policy loading functions
    #

    def _load_policy(self, filename):
        """Load the specified policy."""
        self.log.info("Opening SELinux policy \"{0}\"".format(filename))

        try:
            self.policy = qpol.qpol_policy_factory(str(filename))
        except SyntaxError as err:
            raise exception.InvalidPolicy("Error opening policy file \"{0}\": {1}".
                                          format(filename, err))

        self.log.info("Successfully opened SELinux policy \"{0}\"".format(filename))
        self.filename = filename

    @staticmethod
    def _potential_policies():
        """Generate a list of potential policies to use."""
        # try libselinux for current policy
        if selinux.selinuxfs_exists():
            yield selinux.selinux_current_policy_path()

        # otherwise look through the supported policy versions
        base_policy_path = selinux.selinux_binary_policy_path()
        for version in range(qpol.QPOL_POLICY_MAX_VERSION, qpol.QPOL_POLICY_MIN_VERSION-1, -1):
            yield "{0}.{1}".format(base_policy_path, version)

    def _load_running_policy(self):
        """Try to load the current running policy."""
        self.log.info("Attempting to locate current running policy.")

        for filename in self._potential_policies():
            try:
                self._load_policy(filename)
            except OSError as err:
                if err.errno != ENOENT:
                    raise
            else:
                break
        else:
            raise RuntimeError("Unable to locate an SELinux policy to load.")

    #
    # Policy properties
    #
    @property
    def handle_unknown(self):
        """The handle unknown permissions setting (allow,deny,reject)"""
        return HandleUnknown(self.policy.handle_unknown())

    @property
    def mls(self):
        """(T/F) The policy has MLS enabled."""
        return mls.enabled(self.policy)

    @property
    def version(self):
        """The policy database version (e.g. v29)"""
        return self.policy.version()

    @property
    def target_platform(self):
        """The policy platform (selinux or xen)"""
        return PolicyTarget(self.policy.target_platform())

    #
    # Policy statistics
    #

    @property
    def allow_count(self):
        """The number of (type) allow rules."""
        return self.policy.avrule_allow_count()

    @property
    def allowxperm_count(self):
        """The number of allowxperm rules."""
        return self.policy.avrule_allowx_count()

    @property
    def auditallow_count(self):
        """The number of auditallow rules."""
        return self.policy.avrule_auditallow_count()

    @property
    def auditallowxperm_count(self):
        """The number of auditallowxperm rules."""
        return self.policy.avrule_auditallowx_count()

    @property
    def boolean_count(self):
        """The number of Booleans."""
        return self.policy.bool_count()

    @property
    def category_count(self):
        """The number of categories."""
        return sum(1 for _ in self.categories())

    @property
    def class_count(self):
        """The number of object classes."""
        return self.policy.class_count()

    @property
    def common_count(self):
        """The number of common permission sets."""
        return self.policy.common_count()

    @property
    def conditional_count(self):
        """The number of conditionals."""
        return self.policy.cond_count()

    @property
    def constraint_count(self):
        """The number of standard constraints."""
        return sum(1 for c in self.constraints()
                   if c.ruletype == constraint.ConstraintRuletype.constrain)

    @property
    def default_count(self):
        """The number of default_* rules."""
        return sum(1 for d in self.defaults())

    @property
    def devicetreecon_count(self):
        """The number of Xen devicetreecon statements."""
        return self.policy.devicetreecon_count()

    @property
    def dontaudit_count(self):
        """The number of dontaudit rules."""
        return self.policy.avrule_dontaudit_count()

    @property
    def dontauditxperm_count(self):
        """The number of dontauditxperm rules."""
        return self.policy.avrule_dontauditx_count()

    @property
    def fs_use_count(self):
        """fs_use_* statements."""
        return self.policy.fs_use_count()

    @property
    def genfscon_count(self):
        """The number of genfscon statements."""
        return self.policy.genfscon_count()

    @property
    def initialsids_count(self):
        """The number of initial sid statements."""
        return self.policy.isid_count()

    @property
    def iomemcon_count(self):
        """The number of Xen iomemcon statements."""
        return self.policy.iomemcon_count()

    @property
    def ioportcon_count(self):
        """The number of Xen ioportcon statements."""
        return self.policy.ioportcon_count()

    @property
    def level_count(self):
        """The number of levels."""
        return sum(1 for _ in self.levels())

    @property
    def mlsconstraint_count(self):
        """The number of MLS constraints."""
        return sum(1 for c in self.constraints()
                   if c.ruletype == constraint.ConstraintRuletype.mlsconstrain)

    @property
    def mlsvalidatetrans_count(self):
        """The number of MLS validatetrans."""
        return sum(1 for v in self.constraints()
                   if v.ruletype == constraint.ConstraintRuletype.mlsvalidatetrans)

    @property
    def netifcon_count(self):
        """The number of netifcon statements."""
        return self.policy.netifcon_count()

    @property
    def neverallow_count(self):
        """The number of neverallow rules."""
        return self.policy.avrule_neverallow_count()

    @property
    def neverallowxperm_count(self):
        """The number of neverallowxperm rules."""
        return self.policy.avrule_neverallowx_count()

    @property
    def nodecon_count(self):
        """The number of nodecon statements."""
        return self.policy.nodecon_count()

    @property
    def pcidevicecon_count(self):
        """The number of Xen pcidevicecon statements."""
        return self.policy.pcidevicecon_count()

    @property
    def permission_count(self):
        """The number of permissions."""
        return sum(len(c.perms) for c in chain(self.commons(), self.classes()))

    @property
    def permissives_count(self):
        """The number of permissive types."""
        return self.policy.permissive_count()

    @property
    def pirqcon_count(self):
        """The number of Xen pirqcon statements."""
        return self.policy.pirqcon_count()

    @property
    def polcap_count(self):
        """The number of policy capabilities."""
        return self.policy.polcap_count()

    @property
    def portcon_count(self):
        """The number of portcon statements."""
        return self.policy.portcon_count()

    @property
    def range_transition_count(self):
        """The number of range_transition rules."""
        return self.policy.range_trans_count()

    @property
    def role_count(self):
        """The number of roles."""
        return self.policy.role_count()

    @property
    def role_allow_count(self):
        """The number of (role) allow rules."""
        return self.policy.role_allow_count()

    @property
    def role_transition_count(self):
        """The number of role_transition rules."""
        return self.policy.role_trans_count()

    @property
    def type_attribute_count(self):
        """The number of (type) attributes."""
        return sum(1 for _ in self.typeattributes())

    @property
    def type_count(self):
        """The number of types."""
        return sum(1 for _ in self.types())

    @property
    def type_change_count(self):
        """The number of type_change rules."""
        return self.policy.terule_change_count()

    @property
    def type_member_count(self):
        """The number of type_member rules."""
        return self.policy.terule_member_count()

    @property
    def type_transition_count(self):
        """The number of type_transition rules."""
        return self.policy.terule_trans_count() + self.policy.filename_trans_count()

    @property
    def typebounds_count(self):
        """The number of typebounds rules."""
        return sum(1 for b in self.bounds()
                   if b.ruletype == bounds.BoundsRuletype.typebounds)

    @property
    def user_count(self):
        """The number of users."""
        return self.policy.user_count()

    @property
    def validatetrans_count(self):
        """The number of validatetrans."""
        return sum(1 for v in self.constraints()
                   if v.ruletype == constraint.ConstraintRuletype.validatetrans)

    #
    # Policy components lookup functions
    #
    def lookup_boolean(self, name):
        """Look up a Boolean."""
        return boolcond.boolean_factory(self.policy, name)

    def lookup_class(self, name):
        """Look up an object class."""
        return objclass.class_factory(self.policy, name)

    def lookup_common(self, name):
        """Look up a common permission set."""
        return objclass.common_factory(self.policy, name)

    def lookup_initialsid(self, name):
        """Look up an initial sid."""
        return initsid.initialsid_factory(self.policy, name)

    def lookup_level(self, level):
        """Look up a MLS level."""
        return mls.level_factory(self.policy, level)

    def lookup_sensitivity(self, name):
        """Look up a MLS sensitivity by name."""
        return mls.sensitivity_factory(self.policy, name)

    def lookup_range(self, range_):
        """Look up a MLS range."""
        return mls.range_factory(self.policy, range_)

    def lookup_role(self, name):
        """Look up a role by name."""
        return role.role_factory(self.policy, name)

    def lookup_type(self, name):
        """Look up a type by name."""
        return typeattr.type_factory(self.policy, name, deref=True)

    def lookup_type_or_attr(self, name):
        """Look up a type or type attribute by name."""
        return typeattr.type_or_attr_factory(self.policy, name, deref=True)

    def lookup_typeattr(self, name):
        """Look up a type attribute by name."""
        return typeattr.attribute_factory(self.policy, name)

    def lookup_user(self, name):
        """Look up a user by name."""
        return user.user_factory(self.policy, name)

    #
    # Policy components generators
    #

    def bools(self):
        """Generator which yields all Booleans."""
        for bool_ in self.policy.bool_iter():
            yield boolcond.boolean_factory(self.policy, bool_)

    def bounds(self):
        """Generator which yields all *bounds statements (typebounds, etc.)"""
        for bound in self.policy.typebounds_iter():
            yield bounds.bounds_factory(self.policy, bound)

    def categories(self):
        """Generator which yields all MLS categories."""
        for cat in self.policy.cat_iter():
            try:
                yield mls.category_factory(self.policy, cat)
            except TypeError:
                # libqpol unfortunately iterates over aliases too
                pass

    def classes(self):
        """Generator which yields all object classes."""
        for class_ in self.policy.class_iter():
            yield objclass.class_factory(self.policy, class_)

    def commons(self):
        """Generator which yields all commons."""
        for common in self.policy.common_iter():
            yield objclass.common_factory(self.policy, common)

    def defaults(self):
        """Generator which yields all default_* statements."""
        for default_ in self.policy.default_iter():
            try:
                for default_obj in default.default_factory(self.policy, default_):
                    yield default_obj
            except exception.NoDefaults:
                # qpol iterates over all classes. Handle case
                # where a class has no default_* settings.
                pass

    def levels(self):
        """Generator which yields all level declarations."""
        for level in self.policy.level_iter():

            try:
                yield mls.level_decl_factory(self.policy, level)
            except TypeError:
                # libqpol unfortunately iterates over levels and sens aliases
                pass

    def polcaps(self):
        """Generator which yields all policy capabilities."""
        for cap in self.policy.polcap_iter():
            yield polcap.polcap_factory(self.policy, cap)

    def roles(self):
        """Generator which yields all roles."""
        for role_ in self.policy.role_iter():
            yield role.role_factory(self.policy, role_)

    def sensitivities(self):
        """Generator which yields all sensitivities."""
        # see mls.py for more info on why level_iter is used here.
        for sens in self.policy.level_iter():
            try:
                yield mls.sensitivity_factory(self.policy, sens)
            except TypeError:
                # libqpol unfortunately iterates over sens and aliases
                pass

    def types(self):
        """Generator which yields all types."""
        for type_ in self.policy.type_iter():
            try:
                yield typeattr.type_factory(self.policy, type_)
            except TypeError:
                # libqpol unfortunately iterates over attributes and aliases
                pass

    def typeattributes(self):
        """Generator which yields all (type) attributes."""
        for type_ in self.policy.type_iter():
            try:
                yield typeattr.attribute_factory(self.policy, type_)
            except TypeError:
                # libqpol unfortunately iterates over attributes and aliases
                pass

    def users(self):
        """Generator which yields all users."""
        for user_ in self.policy.user_iter():
            yield user.user_factory(self.policy, user_)

    #
    # Policy rules generators
    #
    def mlsrules(self):
        """Generator which yields all MLS rules."""
        for rule in self.policy.range_trans_iter():
            yield mlsrule.mls_rule_factory(self.policy, rule)

    def rbacrules(self):
        """Generator which yields all RBAC rules."""
        for rule in chain(self.policy.role_allow_iter(),
                          self.policy.role_trans_iter()):
            yield rbacrule.rbac_rule_factory(self.policy, rule)

    def terules(self):
        """Generator which yields all type enforcement rules."""
        for rule in chain(self.policy.avrule_iter(),
                          self.policy.avrulex_iter(),
                          self.policy.terule_iter(),
                          self.policy.filename_trans_iter()):
            yield terule.te_rule_factory(self.policy, rule)

    #
    # Policy rule type validators
    #
    @staticmethod
    def validate_bounds_ruletype(types):
        """Validate constraint types."""
        warnings.warn("Bounds ruletypes have changed to an enumeration.",
                      DeprecationWarning)
        return bounds.validate_ruletype(types)

    @staticmethod
    def validate_constraint_ruletype(types):
        """Validate constraint types."""
        warnings.warn("Constraint ruletypes have changed to an enumeration.",
                      DeprecationWarning)
        return constraint.validate_ruletype(types)

    @staticmethod
    def validate_default_ruletype(types):
        """Validate default_* types."""
        warnings.warn("Default ruletypes have changed to an enumeration.",
                      DeprecationWarning)
        return default.validate_ruletype(types)

    @staticmethod
    def validate_default_value(value):
        """Validate default_* values."""
        warnings.warn("Default values have changed to an enumeration.",
                      DeprecationWarning)
        return default.validate_default_value(value)

    @staticmethod
    def validate_default_range(value):
        """Validate default_range range."""
        warnings.warn("Default range values have changed to an enumeration.",
                      DeprecationWarning)
        return default.validate_default_range(value)

    @staticmethod
    def validate_fs_use_ruletype(types):
        """Validate fs_use_* rule types."""
        return fscontext.validate_ruletype(types)

    @staticmethod
    def validate_mls_ruletype(types):
        """Validate MLS rule types."""
        warnings.warn("MLS ruletypes have changed to an enumeration.",
                      DeprecationWarning)
        return mlsrule.validate_ruletype(types)

    @staticmethod
    def validate_rbac_ruletype(types):
        """Validate RBAC rule types."""
        warnings.warn("RBAC ruletypes have changed to an enumeration.",
                      DeprecationWarning)
        return rbacrule.validate_ruletype(types)

    @staticmethod
    def validate_te_ruletype(types):
        """Validate type enforcement rule types."""
        warnings.warn("TE ruletypes have changed to an enumeration.",
                      DeprecationWarning)
        return terule.validate_ruletype(types)

    #
    # Constraints generators
    #

    def constraints(self):
        """Generator which yields all constraints (regular and MLS)."""
        for constraint_ in chain(self.policy.constraint_iter(),
                                 self.policy.validatetrans_iter()):

            yield constraint.constraint_factory(self.policy, constraint_)

    #
    # In-policy Labeling statement generators
    #
    def fs_uses(self):
        """Generator which yields all fs_use_* statements."""
        for fs_use in self.policy.fs_use_iter():
            yield fscontext.fs_use_factory(self.policy, fs_use)

    def genfscons(self):
        """Generator which yields all genfscon statements."""
        for fscon in self.policy.genfscon_iter():
            yield fscontext.genfscon_factory(self.policy, fscon)

    def initialsids(self):
        """Generator which yields all initial SID statements."""
        for sid in self.policy.isid_iter():
            yield initsid.initialsid_factory(self.policy, sid)

    def netifcons(self):
        """Generator which yields all netifcon statements."""
        for ifcon in self.policy.netifcon_iter():
            yield netcontext.netifcon_factory(self.policy, ifcon)

    def nodecons(self):
        """Generator which yields all nodecon statements."""
        for node in self.policy.nodecon_iter():
            yield netcontext.nodecon_factory(self.policy, node)

    def portcons(self):
        """Generator which yields all portcon statements."""
        for port in self.policy.portcon_iter():
            yield netcontext.portcon_factory(self.policy, port)

    #
    # Xen labeling statements
    #
    def iomemcons(self):
        """Generator which yields all iomemcon statements."""
        for mem_addr in self.policy.iomemcon_iter():
            yield xencontext.iomemcon_factory(self.policy, mem_addr)

    def ioportcons(self):
        """Generator which yields all ioportcon statements."""
        for port in self.policy.ioportcon_iter():
            yield xencontext.ioportcon_factory(self.policy, port)

    def pcidevicecons(self):
        """Generator which yields all pcidevicecon statements."""
        for device in self.policy.pcidevicecon_iter():
            yield xencontext.pcidevicecon_factory(self.policy, device)

    def pirqcons(self):
        """Generator which yields all pirqcon statements."""
        for irq in self.policy.pirqcon_iter():
            yield xencontext.pirqcon_factory(self.policy, irq)

    def devicetreecons(self):
        """Generator which yields all devicetreecon statements."""
        for path in self.policy.devicetreecon_iter():
            yield xencontext.devicetreecon_factory(self.policy, path)
