#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""Apply configuration of EOLE servers.
Password sanity check
"""

import sys
import re
import unicodedata



# a bug in samba-tool does not permit using full range of unicode characters
# http://www.unicode.org/reports/tr44/#General_Category_Values
unicode_categories = set(['Lu', 'Ll', 'Lt', 'LC', 'Lm', 'Lo', 'L',  # Letter
                          'Mn', 'Mc', 'Me', 'M',  # Mark
                          'Nd', 'Nl', 'No', 'N',  # Number
                          'Pc', 'Pd', 'Ps', 'Pe', 'Pi', 'Pf', 'Po', 'P',  # Punctuation
                          'Sm', 'Sc', 'Sk', 'So', 'S',  # Symbol
                          'Zs', 'Zl', 'Zp', 'Z',  # Separator
                          'Cc', 'Cf', 'Cs', 'Co', 'Cn', 'C',  # Other
                          ])

non_alphanumeric_categories = set(['Ps', 'Sm', 'Sk', 'Pc', 'Pd', 'Pe', 'Sc', 'Po'])
unicode_alphabetic_categories = set(['Lu', 'Ll', 'Lt', 'LC', 'Lm', 'Lo', 'L'])
digit_categories = set(['Nd'])
authorized_categories = unicode_alphabetic_categories.union(digit_categories).union(non_alphanumeric_categories)

forbidden_categories = unicode_categories - authorized_categories

european_uppercase_categories = set(['Lu', 'Lt'])
european_lowercase_categories = set(['Ll'])
non_cased_alphabetic_categories = set(['Lo'])

# Character sets
non_alphanumeric = [u'(', u'{', u'[',  # Ds
                    u')', u'}', u']',  # Pe
                    u'~', u'+', u'=', u'|', u'<', u'>',  # Sm
                    u'^', u'`',  # Sk
                    u'_',  # Pc
                    u'-',  # Pd
                    u'$',  # Sc
                    u'!', u'@', u'#', u'%', u'&', u'*', u"'", u'\\', u':', u';', u'"', u"'", u',', u'.', u'?', u'/', u',', u'.',  # Po
                    ]

uppercase_ascii_re = re.compile(r'[A-Z]')
lowercase_ascii_re = re.compile(r'[a-z]')
digits_re = re.compile(r'[0-9]')


def is_non_alphanumeric(c):
    """Return True if character c is valid non-alphanumeric character
    :param c: character to check
    :type c: unicode
    """
    return c in non_alphanumeric


def is_european_uppercase_letter(c):
    """Return True if character c is a european uppercase letter
    :param c: letter to check
    :type c: unicode
    """
    # a bug in samba does not permit full range of european  letters
    return (unicodedata.category(c) in european_uppercase_categories and
            uppercase_ascii_re.match(c) is not None)


def is_european_lowercase_letter(c):
    """Return True if character c is a european lowercase letter
    :param c: letter to check
    :type c: unicode
    """
    # a bug in samba does not permit full range of european  letters
    return (unicodedata.category(c) in european_lowercase_categories and
            lowercase_ascii_re.match(c) is not None)


def is_digit(c):
    """Return True if character c is a digit
    :param c: letter to check
    :type c: unicode
    """
    return digits_re.match(c) is not None


def is_unicode_alphabetic_character(c):
    """Return True if character c is valid unicode alphabetic_character
    :param c: letter to check
    :type c: unicode
    """
    return unicodedata.category(c) in unicode_alphabetic_categories


def is_non_cased_alphabetic_character(c):
    """Return True if character c is valid unicode alphabetic_character
    :param c: letter to check
    :type c: unicode
    """
    return unicodedata.category(c) in non_cased_alphabetic_categories


# Rule list to use in validation process
valid_character_rules = [is_non_alphanumeric,
                         is_european_uppercase_letter,
                         is_european_lowercase_letter,
                         is_digit,
                         #is_non_cased_alphabetic_character,
                         ]


def validated_rules_count(validated_rules):
    """Return number of unique validated rules (number of column with at least
    one True value.
    :param validated_rules: 2-dimensional table with boolean values for each rule, for each character
    :type validated_rules: list of list of boolean
    """
    validated_rules = [any(list(l)) for l in  zip(*validated_rules)]
    return validated_rules.count(True)


def forbidden_characters_in(word, validated_rules):
    """Return list of forbidden characters found in word
    :param word: characters to check
    :type word: unicode
    :param validated_rules: 2-dimensional table with boolean values for each rule, for each character
    :type validated_rules: list of list of boolean
    """
    is_forbidden = [not any(t) for t in validated_rules]
    forbidden_characters = [el for el, t in zip (word, is_forbidden)
                            if t is True]
    return forbidden_characters


def original_checkPassword( pw ):
    """ See https://wiki.samba.org/index.php/Samba_AD_DC_HOWTO
       Containing at least three of the following five character groups
       Uppercase characters of European languages (A through Z, with diacritic marks, Greek and Cyrillic characters)
       Lowercase characters of European languages (a through z, sharp-s, with diacritic marks, Greek and Cyrillic characters)
       Base 10 digits (0 through 9)
       Nonalphanumeric characters: ~!@#$%^&*_-+=`|\\(){}[]:;"'<>,.?/
       Any Unicode character that is categorized as an alphabetic character but is not uppercase or lowercase. This includes Unicode characters from Asian languages.

       If the password doesn't fulfil the complexity requirements, the provisioning will fail and you will have to start over (remove the generated new "smb.conf" in that case).

       Some modifications made to match restrictions applied in code and ldap tree : 4 groups and ASCII mandatory.
       :param pw: password to check
       :type pw: str (utf-8 encoded)
    """

    if len(pw) < 8:
        sys.stderr.write(u"Password must contain at least height characters from four different groups\n")
        sys.stderr.write(u'\n'.join([u'Uppercase ASCII literals',
                                     u'Lowercase ASCII literals',
                                     u'Base 10 digits (0 through 9)',
                                     u'Non alphanumeric character ~!@#$%^&*_-+=`|\\(){}[]:;"\'<>,.?/ ']))
        return False
    validated_rules = [[func(c) for func in valid_character_rules] for c in pw]
    if all([any(vr) for vr in validated_rules]):
        if validated_rules_count(validated_rules) >= 4:  # 4 instead of 3 as stated in wiki to match applied rules
            return True
        else:
            sys.stderr.write(u"Password must contain at least height characters from four different groups\n")
            sys.stderr.write(u'\n'.join([u'Uppercase ASCII literals',
                                        u'Lowercase ASCII literals',
                                        u'Base 10 digits (0 through 9)',
                                        u'Non alphanumeric character ~!@#$%^&*_-+=`|\\(){}[]:;"\'<>,.?/ ']))
            return False
    else:
        forbidden_characters = forbidden_characters_in(pw, validated_rules)
        sys.stderr.write(u"Forbidden character{}: {}\n".format(u's' * (len(forbidden_characters) > 1),
                                                ', '.join(forbidden_characters)))
        return False

def checkPassword( pw ):
    """ See https://wiki.samba.org/index.php/Samba_AD_DC_HOWTO
       Containing at least three of the following five character groups
       Uppercase characters of European languages (A through Z, with diacritic marks, Greek and Cyrillic characters)
       Lowercase characters of European languages (a through z, sharp-s, with diacritic marks, Greek and Cyrillic characters)
       Base 10 digits (0 through 9)
       Nonalphanumeric characters: ~!@#$%^&*_-+=`|\\(){}[]:;"'<>,.?/
       Any Unicode character that is categorized as an alphabetic character but is not uppercase or lowercase. This includes Unicode characters from Asian languages.

       If the password doesn't fulfil the complexity requirements, the provisioning will fail and you will have to start over (remove the generated new "smb.conf" in that case).

       Some modifications made to match restrictions applied in code and ldap tree : 4 groups and ASCII mandatory.
       :param pw: password to check
       :type pw: str (utf-8 encoded)
    """

    if len(pw) < 8:
        #sys.stderr.write(u"Le mot de passe doit contenir au moins 8 caractères de 4 classes différentes\n")
        #sys.stderr.write(u'\n'.join([u'Caractères ASCII majuscules',
        #                             u'Caractères ASCII minuscules',
        #                             u'Caractères numériques (0 à 9)',
        #                             u'Caractères non alphanumeriques ~!@#$%^&*_-+=`|\\(){}[]:;"\'<>,.?/ ']))
        printPasswordRules()
        return False
    validated_rules = [[func(c) for func in valid_character_rules] for c in pw]
    if all([any(vr) for vr in validated_rules]):
        if validated_rules_count(validated_rules) >= 4:  # 4 instead of 3 as stated in wiki to match applied rules
            return True
        else:
            #sys.stderr.write(u"Le mot de passe doit contenir au moins 8 caractères de 4 classes différentes\n")
            #sys.stderr.write(u'\n'.join([u'Caractères ASCII majuscules',
            #                             u'Caractères ASCII minuscules',
            #                             u'Caractères numériques (0 à 9)',
            #                             u'Caractères non alphanumeriques ~!@#$%^&*_-+=`|\\(){}[]:;"\'<>,.?/ ']))
            printPasswordRules()
            return False
    else:
        forbidden_characters = forbidden_characters_in(pw, validated_rules)
        sys.stderr.write(u"\nCaractère interdit{}: {}\n".format(u's' * (len(forbidden_characters) > 1),
                                                ', '.join(set(forbidden_characters))))
        return False

def printPasswordRules():
        sys.stderr.write(u"\nLe mot de passe doit contenir au moins 8 caractères de 4 classes différentes\n")
        sys.stderr.write(u'\n'.join([u'Caractères ASCII majuscules',
                                     u'Caractères ASCII minuscules',
                                     u'Caractères numériques (0 à 9)',
                                     u'Caractères non alphanumeriques ~!@#$%^&*_-+=`|\\(){}[]:;"\'<>,.?/ ']))


if __name__ == '__main__':
    try:
        password = sys.argv[1]
    except:
        sys.stderr.write(u'Aucun mot de passe fourni\n')
        print(1)
        sys.exit(0)
    valid = checkPassword(password)
    if valid is True:
        print(0)
        sys.exit(0)
    else:
        print(1)
        sys.exit(0)
