def test_guard_uses_audit_correctly(audit_log, inquiry, is_allowed, expect_msg): # setup logger consumer log_capture_str = io.StringIO() h = logging.StreamHandler(log_capture_str) h.setFormatter(logging.Formatter( 'msg: %(message)s | effect: %(effect)s | deciders: %(deciders)s | candidates: %(candidates)s | ' + 'inquiry: %(inquiry)s' )) h.setLevel(logging.INFO) audit_log.setLevel(logging.INFO) audit_log.addHandler(h) # setup guard st = MemoryStorage() st.add(PolicyAllow(uid='a', subjects=['Max'], actions=['<.*>'], resources=['<.*>'])) st.add(PolicyAllow(uid='b', subjects=['Max'], actions=['get'], resources=['<.*>'])) st.add(PolicyAllow(uid='c', subjects=['Jim'], actions=['<.*>'], resources=['<.*>'])) st.add(PolicyDeny(uid='d', subjects=['Jim'], actions=['<.*>'], resources=['<.*>'])) st.add(PolicyAllow(uid='e')) g = Guard(st, RegexChecker()) # Run tests assert is_allowed == g.is_allowed(inquiry) result = log_capture_str.getvalue().strip() # a little hack to get rid of <Object ID 4502567760> in inquiry output result = re.sub(r'<Object ID \d+>', '<Object ID some_ID>', result) assert expect_msg == result
def test_find_for_inquiry_with_regex_checker(self, st, policies, inquiry, expected_reference): mem_storage = MemoryStorage() # it returns all stored policies so we consider Guard as a reference for p in policies: st.add(p) mem_storage.add(p) reference_answer = Guard(mem_storage, RegexChecker()).is_allowed(inquiry) assert expected_reference == reference_answer, 'Check reference answer' assert reference_answer == Guard(st, RegexChecker()).is_allowed(inquiry), \ 'SQL storage should give the same answers as reference'
def test_general_flow(self, log_mock): cache_storage = MemoryStorage() back_storage = MemoryStorage() inq = Inquiry(action='get') chk1 = RulesChecker() p1 = Policy(1, description='initial') p2 = Policy(2, description='initial') p3 = Policy(3, description='added later') # initialize backend storage back_storage.add(p1) back_storage.add(p2) # create enfold-cache but do not populate it ec = EnfoldCache(back_storage, cache=cache_storage, populate=False) # make sure policies are returned from the backend assert [p1, p2] == list(ec.find_for_inquiry(inquiry=inq, checker=chk1)) # make sure we logged warning about cache miss log_mock.warning.assert_called_with( '%s cache miss for find_for_inquiry. Trying it from backend storage', 'EnfoldCache') log_mock.reset_mock() # populate cache with backend policies so that we do not make cache hit misses ec.populate() # make sure policies are returned assert [p1, p2] == list(ec.find_for_inquiry(inquiry=inq, checker=chk1)) # make sure we do not have cache misses assert 0 == log_mock.warning.call_count log_mock.reset_mock() # let's add a new policy via enfold-cache ec.add(p3) # make sure policies are returned assert [p1, p2, p3] == list(ec.find_for_inquiry(inquiry=inq, checker=chk1)) # make sure we do not have cache misses assert 0 == log_mock.warning.call_count log_mock.reset_mock() # make sure we have those policies in the backend-storage assert [p1, p2, p3] == list( back_storage.find_for_inquiry(inquiry=inq, checker=chk1)) # make sure we have those policies in the cache-storage assert [p1, p2, p3] == list( cache_storage.find_for_inquiry(inquiry=inq, checker=chk1)) # ----------------------- # ----------------------- # ----------------------- # let's re-create enfold cache. This time with initial population cache_storage2 = MemoryStorage() ec2 = EnfoldCache(back_storage, cache=cache_storage2, populate=True) # make sure we have all policies in the cache-storage assert [p1, p2, p3] == list( cache_storage2.find_for_inquiry(inquiry=inq, checker=chk1)) # make sure policies are returned by find_for_inquiry assert [p1, p2, p3] == list(ec2.find_for_inquiry(inquiry=inq, checker=chk1)) # make sure we do not have cache misses assert 0 == log_mock.warning.call_count log_mock.reset_mock()
def test_init_with_populate_for_dirty_cache_storage(self): cache = MemoryStorage() cache.add(Policy(1)) policies = [Policy(1), Policy(2), Policy(3)] back = Mock(spec=MongoStorage, **{'retrieve_all.side_effect': [policies]}) with pytest.raises(Exception) as excinfo: EnfoldCache(back, cache=cache, populate=True) assert 'Conflicting UID = 1' == str(excinfo.value) assert back.retrieve_all.called
def test_add_return_value(self): cache_storage = MemoryStorage() back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) backend_return = back_storage.add(Policy(1)) ec_return = ec.add(Policy(2)) assert backend_return == ec_return
def test_find_for_inquiry_for_populated_cache(self, log_mock): cache_storage = MemoryStorage() back_storage = MemoryStorage() inq = Inquiry(action='get') chk1 = RulesChecker() p1 = Policy(1, description='foo') p2 = Policy(2, description='bar') cache_storage.add(p1) cache_storage.add(p2) ec = EnfoldCache(back_storage, cache=cache_storage, populate=True) # Make first request assert [p1, p2] == list(ec.find_for_inquiry(inquiry=inq, checker=chk1)) assert 0 == log_mock.warning.call_count log_mock.reset_mock() # Make second request assert [p1, p2] == list(ec.find_for_inquiry(inquiry=inq, checker=chk1)) assert 0 == log_mock.warning.call_count
def test_get_for_non_populated_cache(self, log_mock): cache_storage = MemoryStorage() back_storage = MemoryStorage() p1 = Policy(1, description='foo') p2 = Policy(2, description='bar') back_storage.add(p1) back_storage.add(p2) ec = EnfoldCache(back_storage, cache=cache_storage, populate=False) assert p1 == ec.get(1) log_mock.warning.assert_called_with( '%s cache miss for get Policy with UID=%s. Trying to get it from backend storage', 'EnfoldCache', 1) log_mock.reset_mock() assert p2 == ec.get(2) assert 1 == log_mock.warning.call_count log_mock.reset_mock() # test we won't return any inexistent policies assert ec.get(3) is None
def test_add_fail(self): cache_storage = MemoryStorage() back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) p1 = Policy(1) p2 = Policy(2) p3 = Policy(3) ec.add(p1) back_storage.add = Mock(side_effect=PolicyExistsError('2')) with pytest.raises(PolicyExistsError) as excinfo: ec.add(p2) assert 'Conflicting UID = 2' == str(excinfo.value) assert [p1] == back_storage.get_all(1000, 0) # test general exception back_storage.add = Mock(side_effect=Exception('foo')) with pytest.raises(Exception) as excinfo: ec.add(p3) assert 'foo' == str(excinfo.value) assert [p1] == back_storage.get_all(1000, 0) assert [p1] == cache_storage.get_all(1000, 0)
def test_find_for_inquiry_for_non_populated_cache(self, log_mock): cache_storage = MemoryStorage() back_storage = MemoryStorage() inq = Inquiry(action='get') chk1 = RulesChecker() p1 = Policy(1, description='foo') p2 = Policy(2, description='bar') back_storage.add(p1) back_storage.add(p2) ec = EnfoldCache(back_storage, cache=cache_storage, populate=False) # Make first request assert [p1, p2] == list(ec.find_for_inquiry(inquiry=inq, checker=chk1)) log_mock.warning.assert_called_with( '%s cache miss for find_for_inquiry. Trying it from backend storage', 'EnfoldCache') log_mock.reset_mock() # Make second request assert [p1, p2] == list(ec.find_for_inquiry(inquiry=inq, checker=chk1)) assert 1 == log_mock.warning.call_count log_mock.reset_mock()
def test_guard_logs_inquiry_decision(logger, inquiry, result, expect_message): # set up logging log_capture_str = io.StringIO() h = logging.StreamHandler(log_capture_str) h.setLevel(logging.INFO) logger.setLevel(logging.INFO) logger.addHandler(h) # set up Guard storage = MemoryStorage() storage.add( Policy( uid='1', effect=ALLOW_ACCESS, subjects=['Max'], actions=['watch'], resources=['TV'], )) g = Guard(storage, RegexChecker()) assert result == g.is_allowed(inquiry) log_res = log_capture_str.getvalue().strip() # a little hack to get rid of <Object ID 4502567760> in inquiry output log_res = re.sub(r'<Object ID \d+>', '<Object ID some_ID>', log_res) assert expect_message == log_res
def test_not_allowed_when_similar_policies_have_at_least_one_deny_access(): st = MemoryStorage() policies = ( Policy( uid='1', effect=ALLOW_ACCESS, subjects=['foo'], actions=['bar'], resources=['baz'], ), Policy( uid='2', effect=DENY_ACCESS, subjects=['foo'], actions=['bar'], resources=['baz'], ), ) for p in policies: st.add(p) g = Guard(st, RegexChecker()) assert not g.is_allowed( Inquiry(subject='foo', action='bar', resource='baz'))
def test_guard_can_use_specific_policies_message_class(audit_log): # setup logger consumer log_capture_str = io.StringIO() h = logging.StreamHandler(log_capture_str) h.setFormatter(logging.Formatter('decs: %(deciders)s, candidates: %(candidates)s')) h.setLevel(logging.INFO) audit_log.setLevel(logging.INFO) audit_log.addHandler(h) # setup guard st = MemoryStorage() st.add(PolicyAllow('122')) st.add(PolicyAllow('123', actions=['<.*>'], resources=['<.*>'], subjects=['<.*>'])) st.add(PolicyDeny('124', actions=['<.*>'], resources=['<.*>'], subjects=['<.*>'])) st.add(PolicyDeny('125', actions=['<.*>'], resources=['<.*>'], subjects=['<.*>'])) g = Guard(st, RegexChecker(), audit_policies_cls=PoliciesCountMsg) # Run tests g.is_allowed(Inquiry(action='get', subject='Kim', resource='TV')) assert 'decs: count = 1, candidates: count = 3' == log_capture_str.getvalue().strip()
def test_is_allowed_for_inquiry_match_rules(desc, policy, inquiry, result): storage = MemoryStorage() storage.add(policy) g = Guard(storage, RulesChecker()) assert result == g.is_allowed(inquiry), 'Failed for case: ' + desc
def test_is_allowed(desc, inquiry, should_be_allowed, checker): # Create all required test policies st = MemoryStorage() policies = [ Policy( uid='1', description=""" Max, Nina, Ben, Henry are allowed to create, delete, get the resources only if the client IP matches and the inquiry states that any of them is the resource owner """, effect=ALLOW_ACCESS, subjects=('Max', 'Nina', '<Ben|Henry>'), resources=('myrn:example.com:resource:123', 'myrn:example.com:resource:345', 'myrn:something:foo:<.+>'), actions=('<create|delete>', 'get'), context={ 'ip': CIDR('127.0.0.1/32'), 'owner': SubjectEqual(), }, ), Policy( uid='2', description='Allows Max to update any resource', effect=ALLOW_ACCESS, subjects=['Max'], actions=['update'], resources=['<.*>'], ), Policy( uid='3', description='Max is not allowed to print any resource', effect=DENY_ACCESS, subjects=['Max'], actions=['print'], resources=['<.*>'], ), Policy(uid='4'), Policy( uid='5', description= 'Allows Nina to update any resources that have only digits', effect=ALLOW_ACCESS, subjects=['Nina'], actions=['update'], resources=[r'<[\d]+>'], ), Policy( uid='6', description= 'Allows Nina to update any resources that have only digits. Defined by rules', effect=ALLOW_ACCESS, subjects=[Eq('Nina')], actions=[Eq('update'), Eq('read')], resources=[{ 'id': RegexMatch(r'\d+'), 'magazine': RegexMatch(r'[\d\w]+') }], ), ] for p in policies: st.add(p) g = Guard(st, checker) assert should_be_allowed == g.is_allowed(inquiry)
subjects=['Max'], actions=['print'], resources=['<.*>'], ), Policy(uid='4'), Policy( uid='5', description='Allows Nina to update any resources that have only digits', effect=ALLOW_ACCESS, subjects=['Nina'], actions=['update'], resources=['<[\d]+>'], ), ] for p in policies: st.add(p) @pytest.mark.parametrize('desc, inquiry, should_be_allowed', [ ( 'Empty inquiry carries no information, so nothing is allowed, even empty Policy #4', Inquiry(), False, ), ( 'Max is allowed to update anything', Inquiry(subject='Max', resource='myrn:example.com:resource:123', action='update'), True, ),
def test_policy_inquiry_checker_examples(desc, policy, inquiry, checker, should_be_allowed): storage = MemoryStorage() storage.add(policy) g = Guard(storage, checker) assert should_be_allowed == g.is_allowed(inquiry)