import logging
import re

from roundup.exceptions import Reject

logger = logging.getLogger('detector')


def audit_2fa(db, cl, nodeid, newvalues):
    '''Cases:

        secret_2fa must be 32 characters from base32 alphabet [A-Z2-7]
           or must be '-' (disabled).

        If secret_2fa is '-' secret_counter must be set to -1 in same
           transacation.
        Any other secret_2fa change must set secret_counter to 0 in same
           transacation.
           (Note this may change if TOTP used. Value of -2 reserved for
            counter value in TOTP mode.)

        If the secret_counter is changed without secret_2fa change, counter
           must only be increased. It must not go backwards (to avoid
           replay attacks).
    '''

    if __debug__:
        logger.debug("in auditor: user%s and newvalues: %r", nodeid, newvalues)

    if ('secret_2fa' not in newvalues and
            'secret_counter' not in newvalues):
        # nothing to do
        return

    secret_counter = 0

    if 'secret_2fa' in newvalues:

        # verify that the secret is changed via the gen2fasecret
        # action. This prevents the user from submitting a form
        # that changes the secret to something simple like 32 A's.
        #
        # dbhandle has a creator property added by Generate2faSecret
        # continue if:
        #    db has the Generate2faSecret property. Generate2faSecret action
        #    tx_Source is cli (command line)
        # otherwise abort change.
        if not (hasattr(db, 'Generate2faSecret') or (db.tx_Source in ['cli'])):
            logger.error("Unauthorized path for two factor auth: "
                         "user id %s method %s" % (db.getuid(), db.tx_Source))
            raise Reject("Found unauthorized path for changing "
                         "Two Factor Auth.")

        secret_2fa = newvalues['secret_2fa']

        logger.debug("processing secret_2fa change")
        if 'secret_counter' not in newvalues:
            # make sure it's not set to 0 or -2 in db
            if db.user.get(nodeid, 'secret_counter') not in [0, -2]:
                raise Reject("Two Factor Auth secret changed without "
                             "resetting counter.")
        else:
            secret_counter = newvalues['secret_counter']

        if secret_2fa.password == '-':
            if secret_counter != -1:
                raise Reject("Disabling Two Factor Auth missing proper "
                             "counter value.")
            else:
                return

        if re.match(r'^[A-Z2-7]{32}$', secret_2fa.password):
            # -2 is TOTP sentinal
            if secret_counter not in [0, -2]:
                raise Reject("Resetting Two Factor Auth missing proper "
                             "counter value.")
            else:
                return
        else:
            raise Reject("Invalid secret value for Two Factor Auth")

    # all cases where we are changing the secret_2fa are accounted for.
    vals = {'new': newvalues['secret_counter'],
            'old': db.user.get(nodeid, 'secret_counter')}

    if vals['old'] >= 0:
        if vals['new'] <= vals['old']:
            # if it stays the same it hasn't changed so we won't be called.
            raise Reject("Two Factor Auth secret count must not decrease: "
                         "new: %(new)s, old: %(old)s." % vals)
    else:
        raise Reject("Two Factor Auth is disabled. You must set "
                     "both secret and count at the same time "
                     "to enable it")


def init(db):
    # fire before changes are made
    db.user.audit('set', audit_2fa)
    db.user.audit('create', audit_2fa)

# vim: sts=4 sw=4 et si
