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_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_get_return_value(self): cache_storage = MemoryStorage() back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) p1 = Policy(1) ec.add(p1) backend_return = back_storage.get(1) ec_return = ec.get(1) assert backend_return == ec_return
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_update_return_value(self): cache_storage = MemoryStorage() back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) p1 = Policy(1, description='foo') ec.add(p1) p1.description = 'foo upd' backend_return = back_storage.update(p1) ec_return = ec.update(p1) assert backend_return == ec_return
def test_find_for_inquiry_return_value(self, storage): cache_storage = storage back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) inq = Inquiry(action='get') p1 = Policy(1) p2 = Policy(2) ec.add(p1) ec.add(p2) backend_return = back_storage.find_for_inquiry(inq) ec_return = ec.find_for_inquiry(inq) assert list(backend_return) == list(ec_return)
def test_retrieve_all_return_value(self, storage): cache_storage = storage back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) p1 = Policy(1) p2 = Policy(2) p3 = Policy(3) ec.add(p1) ec.add(p2) ec.add(p3) backend_return = back_storage.retrieve_all() ec_return = back_storage.retrieve_all() assert list(backend_return) == list(ec_return)
def test_get_all_return_value(self, storage): cache_storage = storage back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) p1 = Policy(1) p2 = Policy(2) p3 = Policy(3) ec.add(p1) ec.add(p2) ec.add(p3) backend_return = back_storage.get_all(100, 0) ec_return = back_storage.get_all(100, 0) assert backend_return == ec_return
def test_init_without_populate(self): cache = MemoryStorage() policies = [Policy(1), Policy(2), Policy(3)] back = Mock(spec=MongoStorage, **{'retrieve_all.return_value': [policies]}) c = EnfoldCache(back, cache=cache, populate=False) assert not back.retrieve_all.called assert [] == c.cache.get_all(1000, 0)
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_guard_does_not_log_messages_at_more_than_info_level(audit_log): log_capture_str = io.StringIO() h = logging.StreamHandler(log_capture_str) h.setLevel(logging.WARN) audit_log.setLevel(logging.WARN) audit_log.addHandler(h) g = Guard(MemoryStorage(), RegexChecker()) g.is_allowed(Inquiry()) assert '' == log_capture_str.getvalue().strip()
def test_guard_logs_messages_at_info_level(audit_log): log_capture_str = io.StringIO() h = logging.StreamHandler(log_capture_str) h.setLevel(logging.INFO) audit_log.setLevel(logging.INFO) audit_log.addHandler(h) g = Guard(MemoryStorage(), RegexChecker()) g.is_allowed(Inquiry()) assert 'No potential' in log_capture_str.getvalue().strip()
def test_init_with_populate_and_populate_is_true_by_default(self): cache = MemoryStorage() policies = [Policy(1), Policy(2), Policy(3)] back = Mock(spec=MongoStorage, **{ 'retrieve_all.side_effect': [policies], 'get_all.side_effect': [] }) ec = EnfoldCache(back, cache=cache) assert back.retrieve_all.called assert policies == ec.cache.get_all(10000, 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_add_ok(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) assert [p1] == cache_storage.get_all(100, 0) assert [p1] == back_storage.get_all(100, 0) ec.add(p2) ec.add(p3) assert [p1, p2, p3] == cache_storage.get_all(100, 0) assert [p1, p2, p3] == back_storage.get_all(100, 0)
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_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_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_delete_fail(self): cache_storage = MemoryStorage() back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) p1 = Policy(1, description='foo') p2 = Policy(2, description='bar') ec.add(p1) ec.add(p2) back_storage.delete = Mock(side_effect=Exception('error!')) with pytest.raises(Exception) as excinfo: ec.delete(2) assert 'error!' == str(excinfo.value) assert [p1, p2] == back_storage.get_all(1000, 0) assert [p1, p2] == cache_storage.get_all(1000, 0)
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_delete_ok(self): cache_storage = MemoryStorage() back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) p1 = Policy(1, description='foo') p2 = Policy(2, description='bar') ec.add(p1) ec.add(p2) ec.delete(1) ec.delete(2) assert [] == cache_storage.get_all(100, 0) assert [] == back_storage.get_all(100, 0)
def test_cache_is_invalidated_on_policy_change(self): def assert_after_modification(): assert 0 == cache.info().hits assert 0 == cache.info().misses assert 0 == cache.info().currsize assert not guard.is_allowed(inq1) assert not guard.is_allowed(inq1) assert guard.is_allowed(inq2) assert guard.is_allowed(inq2) assert guard.is_allowed(inq2) assert 3 == cache.info().hits assert 2 == cache.info().misses assert 2 == cache.info().currsize inq1 = Inquiry(action='get', resource='book', subject='Max') inq2 = Inquiry(action='get', resource='book', subject='Jim') guard, storage, cache = create_cached_guard(MemoryStorage(), RulesChecker(), maxsize=256) p1 = Policy(1, actions=[Eq('get')], resources=[Eq('book')], subjects=[Eq('Max')], effect=ALLOW_ACCESS) p2 = Policy(2, actions=[Eq('get')], resources=[Eq('magazine')], subjects=[Eq('Max')], effect=ALLOW_ACCESS) storage.add(p1) assert guard.is_allowed(inq1) assert guard.is_allowed(inq1) assert 1 == cache.info().hits assert 1 == cache.info().misses assert 1 == cache.info().currsize # start modifications p1.subjects = [Eq('Jim')] storage.update(p1) assert_after_modification() storage.add(p2) assert_after_modification() storage.delete(p2) assert_after_modification()
def test_same_inquiries_are_cached(self): guard, storage, cache = create_cached_guard(MemoryStorage(), RulesChecker(), maxsize=256) p1 = Policy(1, actions=[Eq('get')], resources=[Eq('book')], subjects=[Eq('Max')], effect=ALLOW_ACCESS) storage.add(p1) inq1 = Inquiry(action='get', resource='book', subject='Max') inq2 = Inquiry(action='get', resource='book', subject='Jamey') assert guard.is_allowed(inq1) assert guard.is_allowed(inq1) assert guard.is_allowed(inq1) assert guard.is_allowed(inq1) assert guard.is_allowed(inq1) assert not guard.is_allowed(inq2) assert 4 == cache.info().hits assert 2 == cache.info().misses assert 2 == cache.info().currsize
def test_update_ok(self): cache_storage = MemoryStorage() back_storage = MemoryStorage() ec = EnfoldCache(back_storage, cache=cache_storage) p1 = Policy(1, description='foo') p2 = Policy(2, description='bar') p3 = Policy(3, description='baz') ec.add(p1) ec.add(p2) ec.add(p3) p1.description = 'foo2' ec.update(p1) p2.description = 'bar2' ec.update(p2) assert [p1, p2, p3] == cache_storage.get_all(100, 0) assert [p1, p2, p3] == back_storage.get_all(100, 0)
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 st(): return MemoryStorage()
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_for_none_policies(): g = Guard(MemoryStorage(), RegexChecker()) assert not g.is_allowed( Inquiry(subject='foo', action='bar', resource='baz'))
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)