<commit_before># -*- coding: utf-8 -*-
#  2017-08-11 <NAME> <<EMAIL>>
#             Add condition for detail->error->message
#  2017-07-19 <NAME> <<EMAIL>>
#             Add possibility to compare tokeninfo field against fixed time
#             and also {now} with offset.
#  2016-05-04 <NAME> <<EMAIL>>
#             Initial writup
#
# License:  AGPLv3
# (c) 2016. <NAME>
#
# This code is free software; you can redistribute it and/or
# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
# License as published by the Free Software Foundation; either
# version 3 of the License, or any later version.
#
# This code 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 AFFERO GENERAL PUBLIC LICENSE for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#
__doc__ = """This is the base class for an event handler module.
The event handler module is bound to an event together with

* a condition and
* an action
* optional options ;-)
"""
from privacyidea.lib import _
from privacyidea.lib.config import get_token_types
from privacyidea.lib.realm import get_realms
from privacyidea.lib.auth import ROLE
from privacyidea.lib.policy import ACTION
from privacyidea.lib.token import get_token_owner, get_tokens
from privacyidea.lib.user import User, UserError
from privacyidea.lib.utils import (compare_condition, compare_value_value,
                                   parse_time_offset_from_now, is_true)
import datetime
from dateutil.tz import tzlocal
import re
import json
import logging
from privacyidea.lib.tokenclass import DATE_FORMAT

log = logging.getLogger(__name__)


class CONDITION(object):
    """
    Possible conditions
    """
    TOKEN_HAS_OWNER = "token_has_owner"
    TOKEN_IS_ORPHANED = "token_is_orphaned"
    TOKEN_VALIDITY_PERIOD = "token_validity_period"
    USER_TOKEN_NUMBER = "user_token_number"
    OTP_COUNTER = "otp_counter"
    TOKENTYPE = "tokentype"
    LAST_AUTH = "last_auth"
    COUNT_AUTH = "count_auth"
    COUNT_AUTH_SUCCESS = "count_auth_success"
    COUNT_AUTH_FAIL = "count_auth_fail"
    TOKENINFO = "tokeninfo"
    DETAIL_ERROR_MESSAGE = "detail_error_message"
    DETAIL_MESSAGE = "detail_message"
    RESULT_VALUE = "result_value"
    RESULT_STATUS = "result_status"


class BaseEventHandler(object):
    """
    An Eventhandler needs to return a list of actions, which it can handle.

    It also returns a list of allowed action and conditions

    It returns an identifier, which can be used in the eventhandlig definitions
    """

    identifier = "BaseEventHandler"
    description = "This is the base class of an EventHandler with no " \
                  "functionality"

    def __init__(self):
        pass

    @property
    def allowed_positions(cls):
        """
        This returns the allowed positions of the event handler definition.
        This can be "post" or "pre" or both.
        :return: list of allowed positions
        """
        return ["post"]

    @property
    def actions(cls):
        """
        This method returns a list of available actions, that are provided
        by this event handler.
        :return: dictionary of actions.
        """
        actions = ["sample_action_1", "sample_action_2"]
        return actions

    @property
    def conditions(cls):
        """
        The UserNotification can filter for conditions like
        * type of logged in user and
        * successful or failed value.success

        allowed types are str, multi, text, regexp

        :return: dict
        """
        realms = get_realms()
        cond = {
            "realm": {
                "type": "str",
                "desc": _("The user realm, for which this event should apply."),
                "value": realms.keys()
            },
            "tokenrealm": {
                "type": "multi",
                "desc": _("The token realm, for which this event should "
                          "apply."),
                "value": [{"name": r} for r in realms.keys()]
            },
            CONDITION.TOKENTYPE: {
                "type": "multi",
                "desc": _("The type of the token."),
                "value": [{"name": r} for r in get_token_types()]
            },
            "logged_in_user": {
                "type": "str",
                "desc": _("The logged in user is of the following type."),
                "value": (ROLE.ADMIN, ROLE.USER)
            },
            CONDITION.RESULT_VALUE: {
                "type": "str",
                "desc": _("The result.value within the response is "
                          "True or False."),
                "value": ("True", "False")
            },
            CONDITION.RESULT_STATUS: {
                "type": "str",
                "desc": _("The result.status within the response is "
                          "True or False."),
                "value": ("True", "False")
            },
            "token_locked": {
                "type": "str",
                "desc": _("Check if the max failcounter of the token is "
                          "reached."),
                "value": ("True", "False")
            },
            CONDITION.TOKEN_HAS_OWNER: {
                "type": "str",
                "desc": _("The token has a user assigned."),
                "value": ("True", "False")
            },
            CONDITION.TOKEN_IS_ORPHANED: {
                "type": "str",
                "desc": _("The token has a user assigned, but the user does "
                          "not exist in the userstore anymore."),
                "value": ("True", "False")
            },
            CONDITION.TOKEN_VALIDITY_PERIOD: {
                "type": "str",
                "desc": _("Check if the token is within its validity period."),
                "value": ("True", "False")
            },
            "serial": {
                "type": "regexp",
                "desc": _("Action is triggered, if the serial matches this "
                          "regular expression.")
            },
            CONDITION.USER_TOKEN_NUMBER: {
                "type": "str",
                "desc": _("Action is triggered, if the user has this number "
                          "of tokens assigned.")
            },
            CONDITION.OTP_COUNTER: {
                "type": "str",
                "desc": _("Action is triggered, if the counter of the token "
                          "equals this setting.")
            },
            CONDITION.LAST_AUTH: {
                "type": "str",
                "desc": _("Action is triggered, if the last authentication of "
                          "the token is older than 7h, 10d or 1y.")
            },
            CONDITION.COUNT_AUTH: {
                "type": "str",
                "desc": _("This can be '>100', '<99', or '=100', to trigger "
                          "the action, if the tokeninfo field 'count_auth' is "
                          "bigger than 100, less than 99 or exactly 100.")
            },
            CONDITION.COUNT_AUTH_SUCCESS: {
                "type": "str",
                "desc": _("This can be '>100', '<99', or '=100', to trigger "
                          "the action, if the tokeninfo field "
                          "'count_auth_success' is "
                          "bigger than 100, less than 99 or exactly 100.")
            },
            CONDITION.COUNT_AUTH_FAIL: {
                "type": "str",
                "desc": _("This can be '>100', '<99', or '=100', to trigger "
                          "the action, if the difference between the tokeninfo "
                          "field 'count_auth' and 'count_auth_success is "
                          "bigger than 100, less than 99 or exactly 100.")
            },
            CONDITION.TOKENINFO: {
                "type": "str",
                "desc": _("This condition can check any arbitrary tokeninfo "
                          "field. You need to enter something like "
                          "'<fieldname> == <fieldvalue>', '<fieldname> > "
                          "<fieldvalue>' or '<fieldname> < <fieldvalue>'")
            },
            CONDITION.DETAIL_ERROR_MESSAGE: {
                "type": "str",
                "desc": _("Here you can enter a regular expression. The "
                          "condition only applies if the regular expression "
                          "matches the detail->error->message in the response.")
            },
            CONDITION.DETAIL_MESSAGE: {
                "type": "str",
                "desc": _("Here you can enter a regular expression. The "
                          "condition only applies if the regular expression "
                          "matches the detail->message in the response.")
            }
        }
        return cond

    @property
    def events(cls):
        """
        This method returns a list allowed events, that this event handler
        can be bound to and which it can handle with the corresponding actions.

        An eventhandler may return an asterisk ["*"] indicating, that it can
        be used in all events.
        :return: list of events
        """
        events = ["*"]
        return events

    @staticmethod
    def _get_tokenowner(request):
        user = request.User
        serial = request.all_data.get("serial")
        if user.is_empty() and serial:
            # maybe the user is empty, but a serial was passed.
            # Then we determine the user by the serial
            try:
                user = get_token_owner(serial) or User()
            except Exception as exx:
                user = User()
                # This can happen for orphaned tokens.
                log.info("Could not determine tokenowner for {0!s}. Maybe the "
                         "user does not exist anymore.".format(serial))
                log.debug(exx)
        # We now check, if the user exists at all!
        try:
            ui = user.info
        except UserError as exx:
            if exx.id in [904, 905]:
                # 904: User can not be found - maybe we got here due to PassOnNoUser
                # 905: This would be a parameter error - why is this here?
                user = User()
            else:
                raise exx
        return user

    @staticmethod
    def _get_response_content(response):
        if response:
            content = json.loads(response.data)
        else:
            # In Pre-Handling we have no response and no content
            content = {}
        return content<commit_msg>Avoid userstore request in event handler<commit_after># -*- coding: utf-8 -*-
#  2017-08-11 <NAME> <<EMAIL>>
#             Add condition for detail->error->message
#  2017-07-19 <NAME> <<EMAIL>>
#             Add possibility to compare tokeninfo field against fixed time
#             and also {now} with offset.
#  2016-05-04 <NAME> <<EMAIL>>
#             Initial writup
#
# License:  AGPLv3
# (c) 2016. <NAME>
#
# This code is free software; you can redistribute it and/or
# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
# License as published by the Free Software Foundation; either
# version 3 of the License, or any later version.
#
# This code 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 AFFERO GENERAL PUBLIC LICENSE for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#
__doc__ = """This is the base class for an event handler module.
The event handler module is bound to an event together with

* a condition and
* an action
* optional options ;-)
"""
from privacyidea.lib import _
from privacyidea.lib.config import get_token_types
from privacyidea.lib.realm import get_realms
from privacyidea.lib.auth import ROLE
from privacyidea.lib.policy import ACTION
from privacyidea.lib.token import get_token_owner, get_tokens
from privacyidea.lib.user import User, UserError
from privacyidea.lib.utils import (compare_condition, compare_value_value,
                                   parse_time_offset_from_now, is_true)
import datetime
from dateutil.tz import tzlocal
import re
import json
import logging
from privacyidea.lib.tokenclass import DATE_FORMAT

log = logging.getLogger(__name__)


class CONDITION(object):
    """
    Possible conditions
    """
    TOKEN_HAS_OWNER = "token_has_owner"
    TOKEN_IS_ORPHANED = "token_is_orphaned"
    TOKEN_VALIDITY_PERIOD = "token_validity_period"
    USER_TOKEN_NUMBER = "user_token_number"
    OTP_COUNTER = "otp_counter"
    TOKENTYPE = "tokentype"
    LAST_AUTH = "last_auth"
    COUNT_AUTH = "count_auth"
    COUNT_AUTH_SUCCESS = "count_auth_success"
    COUNT_AUTH_FAIL = "count_auth_fail"
    TOKENINFO = "tokeninfo"
    DETAIL_ERROR_MESSAGE = "detail_error_message"
    DETAIL_MESSAGE = "detail_message"
    RESULT_VALUE = "result_value"
    RESULT_STATUS = "result_status"


class BaseEventHandler(object):
    """
    An Eventhandler needs to return a list of actions, which it can handle.

    It also returns a list of allowed action and conditions

    It returns an identifier, which can be used in the eventhandlig definitions
    """

    identifier = "BaseEventHandler"
    description = "This is the base class of an EventHandler with no " \
                  "functionality"

    def __init__(self):
        pass

    @property
    def allowed_positions(cls):
        """
        This returns the allowed positions of the event handler definition.
        This can be "post" or "pre" or both.
        :return: list of allowed positions
        """
        return ["post"]

    @property
    def actions(cls):
        """
        This method returns a list of available actions, that are provided
        by this event handler.
        :return: dictionary of actions.
        """
        actions = ["sample_action_1", "sample_action_2"]
        return actions

    @property
    def conditions(cls):
        """
        The UserNotification can filter for conditions like
        * type of logged in user and
        * successful or failed value.success

        allowed types are str, multi, text, regexp

        :return: dict
        """
        realms = get_realms()
        cond = {
            "realm": {
                "type": "str",
                "desc": _("The user realm, for which this event should apply."),
                "value": realms.keys()
            },
            "tokenrealm": {
                "type": "multi",
                "desc": _("The token realm, for which this event should "
                          "apply."),
                "value": [{"name": r} for r in realms.keys()]
            },
            CONDITION.TOKENTYPE: {
                "type": "multi",
                "desc": _("The type of the token."),
                "value": [{"name": r} for r in get_token_types()]
            },
            "logged_in_user": {
                "type": "str",
                "desc": _("The logged in user is of the following type."),
                "value": (ROLE.ADMIN, ROLE.USER)
            },
            CONDITION.RESULT_VALUE: {
                "type": "str",
                "desc": _("The result.value within the response is "
                          "True or False."),
                "value": ("True", "False")
            },
            CONDITION.RESULT_STATUS: {
                "type": "str",
                "desc": _("The result.status within the response is "
                          "True or False."),
                "value": ("True", "False")
            },
            "token_locked": {
                "type": "str",
                "desc": _("Check if the max failcounter of the token is "
                          "reached."),
                "value": ("True", "False")
            },
            CONDITION.TOKEN_HAS_OWNER: {
                "type": "str",
                "desc": _("The token has a user assigned."),
                "value": ("True", "False")
            },
            CONDITION.TOKEN_IS_ORPHANED: {
                "type": "str",
                "desc": _("The token has a user assigned, but the user does "
                          "not exist in the userstore anymore."),
                "value": ("True", "False")
            },
            CONDITION.TOKEN_VALIDITY_PERIOD: {
                "type": "str",
                "desc": _("Check if the token is within its validity period."),
                "value": ("True", "False")
            },
            "serial": {
                "type": "regexp",
                "desc": _("Action is triggered, if the serial matches this "
                          "regular expression.")
            },
            CONDITION.USER_TOKEN_NUMBER: {
                "type": "str",
                "desc": _("Action is triggered, if the user has this number "
                          "of tokens assigned.")
            },
            CONDITION.OTP_COUNTER: {
                "type": "str",
                "desc": _("Action is triggered, if the counter of the token "
                          "equals this setting.")
            },
            CONDITION.LAST_AUTH: {
                "type": "str",
                "desc": _("Action is triggered, if the last authentication of "
                          "the token is older than 7h, 10d or 1y.")
            },
            CONDITION.COUNT_AUTH: {
                "type": "str",
                "desc": _("This can be '>100', '<99', or '=100', to trigger "
                          "the action, if the tokeninfo field 'count_auth' is "
                          "bigger than 100, less than 99 or exactly 100.")
            },
            CONDITION.COUNT_AUTH_SUCCESS: {
                "type": "str",
                "desc": _("This can be '>100', '<99', or '=100', to trigger "
                          "the action, if the tokeninfo field "
                          "'count_auth_success' is "
                          "bigger than 100, less than 99 or exactly 100.")
            },
            CONDITION.COUNT_AUTH_FAIL: {
                "type": "str",
                "desc": _("This can be '>100', '<99', or '=100', to trigger "
                          "the action, if the difference between the tokeninfo "
                          "field 'count_auth' and 'count_auth_success is "
                          "bigger than 100, less than 99 or exactly 100.")
            },
            CONDITION.TOKENINFO: {
                "type": "str",
                "desc": _("This condition can check any arbitrary tokeninfo "
                          "field. You need to enter something like "
                          "'<fieldname> == <fieldvalue>', '<fieldname> > "
                          "<fieldvalue>' or '<fieldname> < <fieldvalue>'")
            },
            CONDITION.DETAIL_ERROR_MESSAGE: {
                "type": "str",
                "desc": _("Here you can enter a regular expression. The "
                          "condition only applies if the regular expression "
                          "matches the detail->error->message in the response.")
            },
            CONDITION.DETAIL_MESSAGE: {
                "type": "str",
                "desc": _("Here you can enter a regular expression. The "
                          "condition only applies if the regular expression "
                          "matches the detail->message in the response.")
            }
        }
        return cond

    @property
    def events(cls):
        """
        This method returns a list allowed events, that this event handler
        can be bound to and which it can handle with the corresponding actions.

        An eventhandler may return an asterisk ["*"] indicating, that it can
        be used in all events.
        :return: list of events
        """
        events = ["*"]
        return events

    @staticmethod
    def _get_tokenowner(request):
        user = request.User
        serial = request.all_data.get("serial")
        if user.is_empty() and serial:
            # maybe the user is empty, but a serial was passed.
            # Then we determine the user by the serial
            try:
                user = get_token_owner(serial) or User()
            except Exception as exx:
                user = User()
                # This can happen for orphaned tokens.
                log.info("Could not determine tokenowner for {0!s}. Maybe the "
                         "user does not exist anymore.".format(serial))
                log.debug(exx)
        # If the user does not exist, we set an empty user
        if not user.exist():
            user = User()

        return user

    @staticmethod
    def _get_response_content(response):
        if response:
            content = json.loads(response.data)
        else:
            # In Pre-Handling we have no response and no content
            content = {}
        return content
{'fraction_non_alphanumeric': 0.09649659064189983, 'fraction_numerical': 0.00865271573007289, 'mean_word_length': 1.643380981976383, 'pattern_counts': {'":': 114, '<': 45, '<?xml version=': 0, '>': 55, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2871177', 'n_tokens_mistral': 5693, 'n_tokens_neox': 5406, 'n_words': 1936}