#!/usr/bin/python3
# ----------------------------------------------------------------------
#    Copyright (C) 2015 Christian Boltz <apparmor@cboltz.de>
#
#    This program is free software; you can redistribute it and/or
#    modify it under the terms of version 2 of the GNU General Public
#    License as published by the Free Software Foundation.
#
#    This program 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 General Public License for more details.
#
# ----------------------------------------------------------------------

import unittest
from collections import namedtuple
from common_test import AATest, setup_all_loops

from apparmor.rule.change_profile import ChangeProfileRule, ChangeProfileRuleset
from apparmor.rule import BaseRule
from apparmor.common import AppArmorException, AppArmorBug
from apparmor.logparser import ReadLog
from apparmor.translations import init_translation
_ = init_translation()

exp = namedtuple('exp', ['audit', 'allow_keyword', 'deny', 'comment',
        'execmode', 'execcond', 'all_execconds', 'targetprofile', 'all_targetprofiles'])

# --- tests for single ChangeProfileRule --- #

class ChangeProfileTest(AATest):
    def _compare_obj(self, obj, expected):
        self.assertEqual(expected.allow_keyword, obj.allow_keyword)
        self.assertEqual(expected.audit, obj.audit)
        self.assertEqual(expected.execmode, obj.execmode)
        self.assertEqual(expected.execcond, obj.execcond)
        self.assertEqual(expected.targetprofile, obj.targetprofile)
        self.assertEqual(expected.all_execconds, obj.all_execconds)
        self.assertEqual(expected.all_targetprofiles, obj.all_targetprofiles)
        self.assertEqual(expected.deny, obj.deny)
        self.assertEqual(expected.comment, obj.comment)

class ChangeProfileTestParse(ChangeProfileTest):
    tests = [
        # rawrule                                            audit  allow  deny   comment        execmode    execcond  all?   targetprof  all?
        ('change_profile,'                             , exp(False, False, False, ''           , None      , None  ,   True , None     , True )),
        ('change_profile /foo,'                        , exp(False, False, False, ''           , None      , '/foo',   False, None     , True )),
        ('change_profile safe /foo,'                   , exp(False, False, False, ''           , 'safe'    , '/foo',   False, None     , True )),
        ('change_profile unsafe /foo,'                 , exp(False, False, False, ''           , 'unsafe'  , '/foo',   False, None     , True )),
        ('change_profile /foo -> /bar,'                , exp(False, False, False, ''           , None      , '/foo',   False, '/bar'   , False)),
        ('change_profile safe /foo -> /bar,'           , exp(False, False, False, ''           , 'safe'    , '/foo',   False, '/bar'   , False)),
        ('change_profile unsafe /foo -> /bar,'         , exp(False, False, False, ''           , 'unsafe'  , '/foo',   False, '/bar'   , False)),
        ('deny change_profile /foo -> /bar, # comment' , exp(False, False, True , ' # comment' , None      , '/foo',   False, '/bar'   , False)),
        ('audit allow change_profile safe /foo,'       , exp(True , True , False, ''           , 'safe'    , '/foo',   False, None     , True )),
        ('change_profile -> /bar,'                     , exp(False, False, False, ''           , None      , None  ,   True , '/bar'   , False)),
        ('audit allow change_profile -> /bar,'         , exp(True , True , False, ''           , None      , None  ,   True , '/bar'   , False)),
        # quoted versions
        ('change_profile "/foo",'                      , exp(False, False, False, ''           , None      , '/foo',   False, None     , True )),
        ('change_profile "/foo" -> "/bar",'            , exp(False, False, False, ''           , None      , '/foo',   False, '/bar'   , False)),
        ('deny change_profile "/foo" -> "/bar", # cmt' , exp(False, False, True, ' # cmt'      , None      , '/foo',   False, '/bar'   , False)),
        ('audit allow change_profile "/foo",'          , exp(True , True , False, ''           , None      , '/foo',   False, None     , True )),
        ('change_profile -> "/bar",'                   , exp(False, False, False, ''           , None      , None  ,   True , '/bar'   , False)),
        ('audit allow change_profile -> "/bar",'       , exp(True , True , False, ''           , None      , None  ,   True , '/bar'   , False)),
        # with globbing and/or named profiles
        ('change_profile,'                             , exp(False, False, False, ''           , None      , None  ,   True , None     , True )),
        ('change_profile /*,'                          , exp(False, False, False, ''           , None      , '/*'  ,   False, None     , True )),
        ('change_profile /* -> bar,'                   , exp(False, False, False, ''           , None      , '/*'  ,   False, 'bar'    , False)),
        ('deny change_profile /** -> bar, # comment'   , exp(False, False, True , ' # comment' , None      , '/**' ,   False, 'bar'    , False)),
        ('audit allow change_profile /**,'             , exp(True , True , False, ''           , None      , '/**' ,   False, None     , True )),
        ('change_profile -> "ba r",'                   , exp(False, False, False, ''           , None      , None  ,   True , 'ba r'   , False)),
        ('audit allow change_profile -> "ba r",'       , exp(True , True , False, ''           , None      , None  ,   True , 'ba r'   , False)),
     ]

    def _run_test(self, rawrule, expected):
        self.assertTrue(ChangeProfileRule.match(rawrule))
        obj = ChangeProfileRule.parse(rawrule)
        self.assertEqual(rawrule.strip(), obj.raw_rule)
        self._compare_obj(obj, expected)

class ChangeProfileTestParseInvalid(ChangeProfileTest):
    tests = [
        ('change_profile -> ,'                     , AppArmorException),
        ('change_profile foo -> ,'                 , AppArmorException),
        ('change_profile notsafe,'                 , AppArmorException),
        ('change_profile safety -> /bar,'          , AppArmorException),
    ]

    def _run_test(self, rawrule, expected):
        self.assertFalse(ChangeProfileRule.match(rawrule))
        with self.assertRaises(expected):
            ChangeProfileRule.parse(rawrule)

class ChangeProfileTestParseFromLog(ChangeProfileTest):
    def test_change_profile_from_log(self):
        parser = ReadLog('', '', '')

        event = 'type=AVC msg=audit(1428699242.551:386): apparmor="DENIED" operation="change_profile" profile="/foo/changeprofile" pid=3459 comm="changeprofile" target="/foo/rename"'

        # libapparmor doesn't understand this log format (from JJ)
        # event = '[   97.492562] audit: type=1400 audit(1431116353.523:77): apparmor="DENIED" operation="change_profile" profile="/foo/changeprofile" pid=3459 comm="changeprofile" target="/foo/rename"'

        parsed_event = parser.parse_event(event)

        self.assertEqual(parsed_event, {
            'request_mask': None,
            'denied_mask': None,
            'error_code': 0,
            'magic_token': 0,
            'parent': 0,
            'profile': '/foo/changeprofile',
            'operation': 'change_profile',
            'resource': None,
            'info': None,
            'aamode': 'REJECTING',
            'time': 1428699242,
            'active_hat': None,
            'pid': 3459,
            'task': 0,
            'attr': None,
            'name2': '/foo/rename', # target
            'name': None,
            'family': None,
            'protocol': None,
            'sock_type': None,
        })

        obj = ChangeProfileRule(None, ChangeProfileRule.ALL, parsed_event['name2'], log_event=parsed_event)

        #              audit  allow  deny   comment        execmode execcond  all?   targetprof     all?
        expected = exp(False, False, False, ''           , None,    None,     True,  '/foo/rename', False)

        self._compare_obj(obj, expected)

        self.assertEqual(obj.get_raw(1), '  change_profile -> /foo/rename,')


class ChangeProfileFromInit(ChangeProfileTest):
    tests = [
        # ChangeProfileRule object                                             audit  allow  deny   comment        execmode execcond    all?   targetprof  all?
        (ChangeProfileRule(None    , '/foo', '/bar', deny=True)          , exp(False, False, True , ''           , None    , '/foo',   False, '/bar'    , False)),
        (ChangeProfileRule(None    , '/foo', '/bar')                     , exp(False, False, False, ''           , None    , '/foo',   False, '/bar'    , False)),
        (ChangeProfileRule('safe'  , '/foo', '/bar')                     , exp(False, False, False, ''           , 'safe'  , '/foo',   False, '/bar'    , False)),
        (ChangeProfileRule('unsafe', '/foo', '/bar')                     , exp(False, False, False, ''           , 'unsafe', '/foo',   False, '/bar'    , False)),
        (ChangeProfileRule(None    , '/foo', ChangeProfileRule.ALL)      , exp(False, False, False, ''           , None  , '/foo',   False,  None     , True )),
        (ChangeProfileRule(None    , ChangeProfileRule.ALL, '/bar')      , exp(False, False, False, ''           , None  , None  ,   True , '/bar'    , False)),
        (ChangeProfileRule(None    , ChangeProfileRule.ALL,
                             ChangeProfileRule.ALL)            , exp(False, False, False, ''           , None, None  ,   True , None      , True )),
    ]

    def _run_test(self, obj, expected):
        self._compare_obj(obj, expected)


class InvalidChangeProfileInit(AATest):
    tests = [
        # init params                     expected exception
        ([None    , '/foo', ''               ]    , AppArmorBug), # empty targetprofile
        ([None    , ''    , '/bar'           ]    , AppArmorBug), # empty execcond
        ([None    , '    ', '/bar'           ]    , AppArmorBug), # whitespace execcond
        ([None    , '/foo', '   '            ]    , AppArmorBug), # whitespace targetprofile
        ([None    , 'xyxy', '/bar'           ]    , AppArmorException), # invalid execcond
        ([None    , dict(), '/bar'           ]    , AppArmorBug), # wrong type for execcond
        ([None    , None  , '/bar'           ]    , AppArmorBug), # wrong type for execcond
        ([None    , '/foo', dict()           ]    , AppArmorBug), # wrong type for targetprofile
        ([None    , '/foo', None             ]    , AppArmorBug), # wrong type for targetprofile
        (['maybe' , '/foo', '/bar'           ]    , AppArmorBug), # invalid keyword for execmode
    ]

    def _run_test(self, params, expected):
        with self.assertRaises(expected):
            ChangeProfileRule(params[0], params[1], params[2])

    def test_missing_params_1(self):
        with self.assertRaises(TypeError):
            ChangeProfileRule()

    def test_missing_params_2(self):
        with self.assertRaises(TypeError):
            ChangeProfileRule('inet')


class InvalidChangeProfileTest(AATest):
    def _check_invalid_rawrule(self, rawrule):
        obj = None
        self.assertFalse(ChangeProfileRule.match(rawrule))
        with self.assertRaises(AppArmorException):
            obj = ChangeProfileRule.parse(rawrule)

        self.assertIsNone(obj, 'ChangeProfileRule handed back an object unexpectedly')

    def test_invalid_net_missing_comma(self):
        self._check_invalid_rawrule('change_profile')  # missing comma

    def test_invalid_net_non_ChangeProfileRule(self):
        self._check_invalid_rawrule('dbus,')  # not a change_profile rule

    def test_empty_net_data_1(self):
        obj = ChangeProfileRule(None, '/foo', '/bar')
        obj.execcond = ''
        # no execcond set, and ALL not set
        with self.assertRaises(AppArmorBug):
            obj.get_clean(1)

    def test_empty_net_data_2(self):
        obj = ChangeProfileRule(None, '/foo', '/bar')
        obj.targetprofile = ''
        # no targetprofile set, and ALL not set
        with self.assertRaises(AppArmorBug):
            obj.get_clean(1)


class WriteChangeProfileTestAATest(AATest):
    tests = [
        #  raw rule                                                      clean rule
        ('     change_profile         ,    # foo        '              , 'change_profile, # foo'),
        ('    audit     change_profile /foo,'                          , 'audit change_profile /foo,'),
        ('   deny change_profile         /foo      -> bar,# foo bar'   , 'deny change_profile /foo -> bar, # foo bar'),
        ('   deny change_profile         /foo      ,# foo bar'         , 'deny change_profile /foo, # foo bar'),
        ('   allow change_profile   ->    /bar     ,# foo bar'         , 'allow change_profile -> /bar, # foo bar'),
        ('   allow change_profile   unsafe  /** ->    /bar     ,# foo bar'     , 'allow change_profile unsafe /** -> /bar, # foo bar'),
        ('   allow change_profile   "/fo o" ->    "/b ar",'            , 'allow change_profile "/fo o" -> "/b ar",'),
    ]

    def _run_test(self, rawrule, expected):
        self.assertTrue(ChangeProfileRule.match(rawrule))
        obj = ChangeProfileRule.parse(rawrule)
        clean = obj.get_clean()
        raw = obj.get_raw()

        self.assertEqual(expected.strip(), clean, 'unexpected clean rule')
        self.assertEqual(rawrule.strip(), raw, 'unexpected raw rule')

    def test_write_manually(self):
        obj = ChangeProfileRule(None, '/foo', 'bar', allow_keyword=True)

        expected = '    allow change_profile /foo -> bar,'

        self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule')
        self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule')


class ChangeProfileCoveredTest(AATest):
    def _run_test(self, param, expected):
        obj = ChangeProfileRule.parse(self.rule)
        check_obj = ChangeProfileRule.parse(param)

        self.assertTrue(ChangeProfileRule.match(param))

        self.assertEqual(obj.is_equal(check_obj), expected[0], 'Mismatch in is_equal, expected %s' % expected[0])
        self.assertEqual(obj.is_equal(check_obj, True), expected[1], 'Mismatch in is_equal/strict, expected %s' % expected[1])

        self.assertEqual(obj.is_covered(check_obj), expected[2], 'Mismatch in is_covered, expected %s' % expected[2])
        self.assertEqual(obj.is_covered(check_obj, True, True), expected[3], 'Mismatch in is_covered/exact, expected %s' % expected[3])

class ChangeProfileCoveredTest_01(ChangeProfileCoveredTest):
    rule = 'change_profile /foo,'

    tests = [
        #   rule                                        equal     strict equal    covered     covered exact
        ('           change_profile,'               , [ False   , False         , False     , False     ]),
        ('           change_profile /foo,'          , [ True    , True          , True      , True      ]),
        ('           change_profile safe /foo,'     , [ True    , False         , True      , True      ]),
        ('           change_profile unsafe /foo,'   , [ False   , False         , False     , False     ]),
        ('           change_profile /foo, # comment', [ True    , False         , True      , True      ]),
        ('     allow change_profile /foo,'          , [ True    , False         , True      , True      ]),
        ('           change_profile     /foo,'      , [ True    , False         , True      , True      ]),
        ('           change_profile /foo -> /bar,'  , [ False   , False         , True      , True      ]),
        ('           change_profile /foo -> bar,'   , [ False   , False         , True      , True      ]),
        ('audit      change_profile /foo,'          , [ False   , False         , False     , False     ]),
        ('audit      change_profile,'               , [ False   , False         , False     , False     ]),
        ('           change_profile /asdf,'         , [ False   , False         , False     , False     ]),
        ('           change_profile -> /bar,'       , [ False   , False         , False     , False     ]),
        ('audit deny change_profile /foo,'          , [ False   , False         , False     , False     ]),
        ('      deny change_profile /foo,'          , [ False   , False         , False     , False     ]),
    ]

class ChangeProfileCoveredTest_02(ChangeProfileCoveredTest):
    rule = 'audit change_profile /foo,'

    tests = [
        #   rule                                       equal     strict equal    covered     covered exact
        (      'change_profile /foo,'              , [ False   , False         , True      , False     ]),
        ('audit change_profile /foo,'              , [ True    , True          , True      , True      ]),
        (      'change_profile /foo -> /bar,'      , [ False   , False         , True      , False     ]),
        (      'change_profile safe /foo -> /bar,' , [ False   , False         , True      , False     ]),
        ('audit change_profile /foo -> /bar,'      , [ False   , False         , True      , True      ]), # XXX is "covered exact" correct here?
        (      'change_profile,'                   , [ False   , False         , False     , False     ]),
        ('audit change_profile,'                   , [ False   , False         , False     , False     ]),
        ('      change_profile -> /bar,'           , [ False   , False         , False     , False     ]),
    ]


class ChangeProfileCoveredTest_03(ChangeProfileCoveredTest):
    rule = 'change_profile /foo -> /bar,'

    tests = [
        #   rule                                       equal     strict equal    covered     covered exact
        (      'change_profile /foo -> /bar,'      , [ True    , True          , True      , True      ]),
        ('allow change_profile /foo -> /bar,'      , [ True    , False         , True      , True      ]),
        (      'change_profile /foo,'              , [ False   , False         , False     , False     ]),
        (      'change_profile,'                   , [ False   , False         , False     , False     ]),
        (      'change_profile /foo -> /xyz,'      , [ False   , False         , False     , False     ]),
        ('audit change_profile,'                   , [ False   , False         , False     , False     ]),
        ('audit change_profile /foo -> /bar,'      , [ False   , False         , False     , False     ]),
        (      'change_profile      -> /bar,'      , [ False   , False         , False     , False     ]),
        (      'change_profile,'                   , [ False   , False         , False     , False     ]),
    ]

class ChangeProfileCoveredTest_04(ChangeProfileCoveredTest):
    rule = 'change_profile,'

    tests = [
        #   rule                                       equal     strict equal    covered     covered exact
        (      'change_profile,'                   , [ True    , True          , True      , True      ]),
        ('allow change_profile,'                   , [ True    , False         , True      , True      ]),
        (      'change_profile /foo,'              , [ False   , False         , True      , True      ]),
        (      'change_profile /xyz -> bar,'       , [ False   , False         , True      , True      ]),
        (      'change_profile -> /bar,'           , [ False   , False         , True      , True      ]),
        (      'change_profile /foo -> /bar,'      , [ False   , False         , True      , True      ]),
        ('audit change_profile,'                   , [ False   , False         , False     , False     ]),
        ('deny  change_profile,'                   , [ False   , False         , False     , False     ]),
    ]

class ChangeProfileCoveredTest_05(ChangeProfileCoveredTest):
    rule = 'deny change_profile /foo,'

    tests = [
        #   rule                                       equal     strict equal    covered     covered exact
        (      'deny change_profile /foo,'         , [ True    , True          , True      , True      ]),
        ('audit deny change_profile /foo,'         , [ False   , False         , False     , False     ]),
        (           'change_profile /foo,'         , [ False   , False         , False     , False     ]), # XXX should covered be true here?
        (      'deny change_profile /bar,'         , [ False   , False         , False     , False     ]),
        (      'deny change_profile,'              , [ False   , False         , False     , False     ]),
    ]

class ChangeProfileCoveredTest_06(ChangeProfileCoveredTest):
    rule = 'change_profile safe /foo,'

    tests = [
        #   rule                                       equal     strict equal    covered     covered exact
        (      'deny change_profile /foo,'         , [ False   , False         , False     , False     ]),
        ('audit deny change_profile /foo,'         , [ False   , False         , False     , False     ]),
        (           'change_profile /foo,'         , [ True    , False         , True      , True      ]),
        (      'deny change_profile /bar,'         , [ False   , False         , False     , False     ]),
        (      'deny change_profile,'              , [ False   , False         , False     , False     ]),
    ]

class ChangeProfileCoveredTest_Invalid(AATest):
    def test_borked_obj_is_covered_1(self):
        obj = ChangeProfileRule.parse('change_profile /foo,')

        testobj = ChangeProfileRule(None, '/foo', '/bar')
        testobj.execcond = ''

        with self.assertRaises(AppArmorBug):
            obj.is_covered(testobj)

    def test_borked_obj_is_covered_2(self):
        obj = ChangeProfileRule.parse('change_profile /foo,')

        testobj = ChangeProfileRule(None, '/foo', '/bar')
        testobj.targetprofile = ''

        with self.assertRaises(AppArmorBug):
            obj.is_covered(testobj)

    def test_invalid_is_covered(self):
        obj = ChangeProfileRule.parse('change_profile /foo,')

        testobj = BaseRule()  # different type

        with self.assertRaises(AppArmorBug):
            obj.is_covered(testobj)

    def test_invalid_is_equal(self):
        obj = ChangeProfileRule.parse('change_profile -> /bar,')

        testobj = BaseRule()  # different type

        with self.assertRaises(AppArmorBug):
            obj.is_equal(testobj)

class ChangeProfileLogprofHeaderTest(AATest):
    tests = [
        ('change_profile,',                         [                                                         _('Exec Condition'), _('ALL'),  _('Target Profile'), _('ALL'),   ]),
        ('change_profile -> /bin/ping,',            [                                                         _('Exec Condition'), _('ALL'),  _('Target Profile'), '/bin/ping',]),
        ('change_profile /bar -> /bin/bar,',        [                                                         _('Exec Condition'), '/bar',    _('Target Profile'), '/bin/bar', ]),
        ('change_profile safe /foo,',                    [                          _('Exec Mode'), 'safe',   _('Exec Condition'), '/foo',    _('Target Profile'), _('ALL'),   ]),
        ('audit change_profile -> /bin/ping,',      [_('Qualifier'), 'audit',                                 _('Exec Condition'), _('ALL'),  _('Target Profile'), '/bin/ping',]),
        ('deny change_profile /bar -> /bin/bar,',   [_('Qualifier'), 'deny',                                  _('Exec Condition'), '/bar',    _('Target Profile'), '/bin/bar', ]),
        ('allow change_profile unsafe /foo,',       [_('Qualifier'), 'allow',       _('Exec Mode'), 'unsafe', _('Exec Condition'), '/foo',    _('Target Profile'), _('ALL'),   ]),
        ('audit deny change_profile,',              [_('Qualifier'), 'audit deny',                            _('Exec Condition'), _('ALL'),  _('Target Profile'), _('ALL'),   ]),
    ]

    def _run_test(self, params, expected):
        obj = ChangeProfileRule.parse(params)
        self.assertEqual(obj.logprof_header(), expected)

# --- tests for ChangeProfileRuleset --- #

class ChangeProfileRulesTest(AATest):
    def test_empty_ruleset(self):
        ruleset = ChangeProfileRuleset()
        ruleset_2 = ChangeProfileRuleset()
        self.assertEqual([], ruleset.get_raw(2))
        self.assertEqual([], ruleset.get_clean(2))
        self.assertEqual([], ruleset_2.get_raw(2))
        self.assertEqual([], ruleset_2.get_clean(2))

    def test_ruleset_1(self):
        ruleset = ChangeProfileRuleset()
        rules = [
            'change_profile -> /bar,',
            'change_profile /foo,',
        ]

        expected_raw = [
            'change_profile -> /bar,',
            'change_profile /foo,',
            '',
        ]

        expected_clean = [
            'change_profile -> /bar,',
            'change_profile /foo,',
            '',
        ]

        for rule in rules:
            ruleset.add(ChangeProfileRule.parse(rule))

        self.assertEqual(expected_raw, ruleset.get_raw())
        self.assertEqual(expected_clean, ruleset.get_clean())

    def test_ruleset_2(self):
        ruleset = ChangeProfileRuleset()
        rules = [
            'change_profile /foo -> /bar,',
            'allow change_profile /asdf,',
            'deny change_profile -> xy, # example comment',
        ]

        expected_raw = [
            '  change_profile /foo -> /bar,',
            '  allow change_profile /asdf,',
            '  deny change_profile -> xy, # example comment',
            '',
        ]

        expected_clean = [
            '  deny change_profile -> xy, # example comment',
            '',
            '  allow change_profile /asdf,',
            '  change_profile /foo -> /bar,',
            '',
        ]

        for rule in rules:
            ruleset.add(ChangeProfileRule.parse(rule))

        self.assertEqual(expected_raw, ruleset.get_raw(1))
        self.assertEqual(expected_clean, ruleset.get_clean(1))


class ChangeProfileGlobTestAATest(AATest):
    def setUp(self):
        self.ruleset = ChangeProfileRuleset()

    def test_glob_1(self):
        self.assertEqual(self.ruleset.get_glob('change_profile /foo,'), 'change_profile,')

    # not supported or used yet, glob behaviour not decided yet
    # def test_glob_2(self):
    #     self.assertEqual(self.ruleset.get_glob('change_profile /foo -> /bar,'), 'change_profile -> /bar,')

    def test_glob_ext(self):
        with self.assertRaises(NotImplementedError):
            # get_glob_ext is not available for change_profile rules
            self.ruleset.get_glob_ext('change_profile /foo -> /bar,')

class ChangeProfileDeleteTestAATest(AATest):
    pass

setup_all_loops(__name__)
if __name__ == '__main__':
    unittest.main(verbosity=1)
