def test_run_as_pop(walter_identifier, jackie_identifier, yosai, jackie_testpermissions, walter_testpermissions, valid_thedude_username_password_token, valid_thedude_totp_token): jp = jackie_testpermissions wp = walter_testpermissions with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) new_subject.run_as(jackie_identifier) jackieresults = new_subject.is_permitted(jp['perms']) assert jackieresults == jp['expected_results'] new_subject.run_as(walter_identifier) walterresults = new_subject.is_permitted(wp['perms']) assert walterresults == wp['expected_results'] new_subject.pop_identity() assert new_subject.identifiers == jackie_identifier new_subject.logout()
def test_subject_mfa_invalid_login_sequence(valid_thedude_totp_token, yosai): with Yosai.context(yosai): new_subject = Yosai.get_current_subject() with pytest.raises(InvalidAuthenticationSequenceException): new_subject.login(valid_thedude_totp_token)
def test_requires_dynamic_permission_fails( yosai, valid_thedude_username_password_token, valid_thedude_totp_token): class BankCheck: def __init__(self): self.bankcheck_id = 'bankcheck_12345' status = None @Yosai.requires_dynamic_permission( ['money:bounce:{bankcheck.bankcheck_id}']) def do_something(bankcheck): nonlocal status status = True with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) with pytest.raises(UnauthorizedException): do_something(bankcheck=BankCheck()) new_subject.logout() assert status is None
def test_check_role_succeeds(thedude_testroles, valid_thedude_username_password_token, valid_thedude_totp_token, yosai, event_bus): tr = thedude_testroles event_detected = None def event_listener(identifiers=None, items=None, logical_operator=None, topic=EVENT_TOPIC): nonlocal event_detected event_detected = items event_bus.subscribe(event_listener, 'AUTHORIZATION.GRANTED') with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) assert (new_subject.check_role(tr['roles'], any) is None and event_detected == list(tr['roles'])) new_subject.logout()
def test_check_role_raises(thedude_testroles, valid_thedude_username_password_token, valid_thedude_totp_token, yosai, event_bus): tr = thedude_testroles event_detected = None def event_listener(identifiers=None, items=None, logical_operator=None, topic=EVENT_TOPIC): nonlocal event_detected event_detected = items event_bus.subscribe(event_listener, 'AUTHORIZATION.DENIED') with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) with pytest.raises(UnauthorizedException): new_subject.check_role(tr['roles'], all) assert event_detected == tr['roles'] new_subject.logout()
def test_authenticated_subject_check_permission_succeeds( thedude_testpermissions, valid_thedude_username_password_token, valid_thedude_totp_token, yosai, event_bus): tp = thedude_testpermissions event_detected = None def event_listener(identifiers=None, items=None, logical_operator=None, topic=EVENT_TOPIC): nonlocal event_detected event_detected = items event_bus.subscribe(event_listener, 'AUTHORIZATION.GRANTED') with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) check = new_subject.check_permission(tp['perms'], any) assert (check is None and event_detected == tp['perms']) new_subject.logout() with pytest.raises(UnauthenticatedException): new_subject.check_permission(tp['perms'], any)
def test_has_role(valid_thedude_username_password_token, thedude_testroles, valid_thedude_totp_token, yosai, event_bus): tr = thedude_testroles event_detected = None def event_listener(identifiers=None, items=None, logical_operator=None, topic=EVENT_TOPIC): nonlocal event_detected event_detected = items event_bus.subscribe(event_listener, 'AUTHORIZATION.RESULTS') with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) result = new_subject.has_role(tr['roles']) assert (tr['expected_results'] == result and set(event_detected) == result) new_subject.logout()
def test_is_permitted(modular_realm_authorizer, thedude_testpermissions, event_bus, thedude_identifier, yosai, valid_thedude_totp_token, valid_thedude_username_password_token): """ get a set of tuple(s), containing the Permission and a Boolean indicating whether the permission is granted """ mra = modular_realm_authorizer tp = thedude_testpermissions event_detected = None def event_listener(identifiers=None, items=None, logical_operator=None, topic=EVENT_TOPIC): nonlocal event_detected event_detected = items event_bus.subscribe(event_listener, 'AUTHORIZATION.RESULTS') with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) results = mra.is_permitted(thedude_identifier, tp['perms']) assert (tp['expected_results'] == results and set(event_detected) == results)
def test_subject_invalid_login(invalid_thedude_username_password_token, yosai): with Yosai.context(yosai): new_subject = Yosai.get_current_subject() with pytest.raises(AuthenticationException): new_subject.login(invalid_thedude_username_password_token)
def test_session_idle_expiration_clears_cache( valid_thedude_username_password_token, thedude_testpermissions, valid_thedude_totp_token, caplog, cache_handler, yosai): tp = thedude_testpermissions with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) new_subject.is_permitted(tp['perms']) # caches authz_info session = new_subject.get_session() session = cache_handler.get('session', identifier=session.session_id) twenty_ago = (60 * 20 * 1000) session.last_access_time = session.last_access_time - twenty_ago cache_handler.set('session', session.session_id, session) session = cache_handler.get('session', identifier=session.session_id) session = new_subject.get_session() with pytest.raises(ExpiredSessionException): session.last_access_time # this triggers the expiration out = caplot.text assert ('Clearing cached credentials for [thedude]' in out and 'Clearing cached authz_info for [thedude]' in out)
def get_current_subject(): try: subject = global_subject_context.stack[-1] msg = ('A subject instance DOES exist in the global context. ' 'Touching and then returning it.') logger.debug(msg) subject.get_session().touch() return subject except IndexError: msg = 'A subject instance _DOES NOT_ exist in the global context. Creating one.' logger.debug(msg) subject = Yosai.get_current_yosai()._get_subject() global_subject_context.stack.append(subject) return subject except ExpiredSessionException as exc: # absolute timeout of remember_me cookies is TBD (idle expired rolls) if WebYosai.get_current_webregistry().remember_me: msg = ('A remembered subject from the global context has an ' 'idle-expired session. Re-creating a new subject ' 'instance/session for it.') logger.debug(msg) global_subject_context.stack.pop() subject = Yosai.get_current_yosai()._get_subject() global_subject_context.stack.append(subject) return subject raise WebYosai.get_current_webregistry().raise_unauthorized(exc)
def test_run_as_raises(walter_identifier, yosai): with Yosai.context(yosai): new_subject = Yosai.get_current_subject() # a login is required , so this should raise: new_subject.logout() with pytest.raises(ValueError): new_subject.run_as(walter_identifier)
def test_authenticated_subject_session_attribute_logout( valid_walter_username_password_token, yosai): with Yosai.context(yosai): new_subject = Yosai.get_current_subject() new_subject.login(valid_walter_username_password_token) session = new_subject.get_session() session.set_attribute('attribute1', 'attr1') session.set_attribute('attribute2', 'attr2') assert (session.get_attribute('attribute1') == 'attr1' and session.get_attribute('attribute2') == 'attr2') new_subject.logout()
def invalid_thedude_username_password_token(yosai, monkeypatch): yield UsernamePasswordToken(username='******', password='******', remember_me=False, host='127.0.0.1') with Yosai.context(yosai): new_subject = Yosai.get_current_subject() da = new_subject.security_manager.authenticator monkeypatch.setattr(da.authc_settings, 'account_lock_threshold', 3) da.init_locking() da.locking_realm.unlock_account('thedude')
def test_singlefactor_subject_locks_at_userpass( invalid_walter_username_password_token, yosai, event_bus, monkeypatch, valid_walter_username_password_token): """ - locks a single-factor account after N attempts - confirms that a locked account will not authenticate userpass """ lock_event_detected = None success_event_detected = None def lock_event_listener(identifier=None, topic=EVENT_TOPIC): nonlocal lock_event_detected lock_event_detected = identifier def success_event_listener(identifier=None, topic=EVENT_TOPIC): nonlocal success_event_detected success_event_detected = identifier event_bus.subscribe(lock_event_listener, 'AUTHENTICATION.ACCOUNT_LOCKED') event_bus.subscribe(success_event_listener, 'AUTHENTICATION.SUCCEEDED') with Yosai.context(yosai): new_subject = Yosai.get_current_subject() da = new_subject.security_manager.authenticator monkeypatch.setattr(da.authc_settings, 'account_lock_threshold', 3) da.init_locking() da.locking_realm.unlock_account('walter') try: new_subject.login(invalid_walter_username_password_token) except AuthenticationException: try: new_subject.login(invalid_walter_username_password_token) except AuthenticationException: try: new_subject.login(invalid_walter_username_password_token) except AuthenticationException: try: new_subject.login( invalid_walter_username_password_token) except LockedAccountException: try: event_bus.subscribe( lock_event_listener, 'AUTHENTICATION.ACCOUNT_LOCKED') event_bus.subscribe(success_event_listener, 'AUTHENTICATION.SUCCEEDED') account_id = da.authenticate_account( None, valid_walter_username_password_token) except LockedAccountException: assert lock_event_detected == 'walter' assert success_event_detected is None da.locking_realm.unlock_account('walter')
def test_totp_replay_attack(yosai, valid_thedude_username_password_token, valid_thedude_totp_token): with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) with pytest.raises(IncorrectCredentialsException): new_subject.login(valid_thedude_totp_token)
def test_mfa_subject_locks_at_totp(valid_thedude_username_password_token, yosai, invalid_thedude_totp_token, valid_thedude_totp_token, event_bus, monkeypatch): """ - locks an account after N attempts during totp authc - confirms that a locked account will not authenticate totp """ lock_event_detected = None success_event_detected = None def lock_event_listener(identifier=None, topic=EVENT_TOPIC): nonlocal lock_event_detected lock_event_detected = identifier def success_event_listener(identifier=None, topic=EVENT_TOPIC): nonlocal success_event_detected success_event_detected = identifier event_bus.subscribe(lock_event_listener, 'AUTHENTICATION.ACCOUNT_LOCKED') event_bus.subscribe(success_event_listener, 'AUTHENTICATION.SUCCEEDED') with Yosai.context(yosai): new_subject = Yosai.get_current_subject() da = new_subject.security_manager.authenticator monkeypatch.setattr(da.authc_settings, 'account_lock_threshold', 3) da.init_locking() da.locking_realm.unlock_account('thedude') try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired as exc: try: new_subject.login(invalid_thedude_totp_token) except AuthenticationException: try: new_subject.login(invalid_thedude_totp_token) except AuthenticationException: try: new_subject.login(invalid_thedude_totp_token) except AuthenticationException: try: new_subject.login(invalid_thedude_totp_token) except LockedAccountException: with pytest.raises(LockedAccountException): new_subject.login(valid_thedude_totp_token) assert lock_event_detected == 'thedude' assert success_event_detected is None da.locking_realm.unlock_account('thedude')
def test_login_clears_cache(thedude_identifier, valid_thedude_username_password_token, caplog, valid_thedude_totp_token, yosai): with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) out = caplog.text assert 'Clearing cached authz_info for [thedude]' in out new_subject.logout()
def inner_wrapper(*args, **kwargs): try: with Yosai.context(self._yosai): # Context Manager functions try: try: identity = self.authenticate(request_proxy) if not identity.username: raise UnauthenticatedError('Authentication Required') except: raise UnauthenticatedError('Authentication Required') ApiRequestContextProxy.set_identity(identity) if self._check_account(identity.user_account, identity.user_account_type, with_names, with_types): return f(*args, **kwargs) finally: # Teardown the request context ApiRequestContextProxy.set_identity(None) except UnauthorizedAccountError as ex: return make_response_error(str(ex), in_httpcode=403), 403 except UnauthenticatedError as ex: return Response(response='Unauthorized', status=401, headers=[('WWW-Authenticate', 'basic realm="Authentication required"')]) except Exception as ex: logger.exception('Unexpected exception: {}'.format(ex)) return make_response_error('Internal error', in_httpcode=500), 500
def inner_wrap(*args, **kwargs): subject = Yosai.get_current_subject() subject.check_role(roleid_s, logical_operator) return fn(*args, **kwargs)
def authenticate_token(self, authc_token=None): if authc_token: subject = Yosai.get_current_subject() try: subject.login(authc_token) except: logger.debug_exception('Login failed') raise user = subject.primary_identifier logger.debug('Login complete for user: {}'.format(user)) if isinstance(user, IdentityContext): return user else: # Simple account lookup to ensure the context identity is complete try: logger.debug('Loading identity context from username: {}'.format(user)) with session_scope() as db_session: idp = self._idp_factory.for_session(db_session) identity, _ = idp.lookup_user(user) logger.debug('Authc complete for user: {}'.format(user)) return identity except: logger.debug_exception('Error looking up account for authenticated user') return None else: logger.debug('Anon auth complete') return IdentityContext(username=None, user_account=None, user_account_type=None, user_account_state=None)
def authenticate(self, request): logger.debug('Authenticating with native auth handler') subject = Yosai.get_current_subject() if request.authorization: authc_token = UsernamePasswordToken( username=request.authorization.username, password=request.authorization.password, remember_me=False) subject.login(authc_token) user = subject.primary_identifier # Simple account lookup to ensure the context identity is complete try: with session_scope() as db_session: idp = self._idp_factory.for_session(db_session) identity, _ = idp.lookup_user(user) logger.debug('Authc complete') return identity except: logger.exception( 'Error looking up account for authenticated user') return None else: logger.debug('Anon auth complete') return IdentityContext(username=None, user_account=None, user_account_type=None, user_account_active=None)
def load(self, configuration): conf_path = pkg_resources.resource_filename( anchore_engine.__name__, 'conf/internal_authz_yosai_settings.yaml') self.yosai = Yosai(file_path=conf_path) # Disable sessions, since the APIs are not session-based self.yosai.security_manager.subject_store.session_storage_evaluator.session_storage_enabled = False
def authorize(self, identity: IdentityContext, permission_list): logger.debug( 'Authorizing with native auth handler: {}'.format(permission_list)) subject = Yosai.get_current_subject() if subject.primary_identifier != identity.username: raise UnauthorizedError(permission_list) # Do account state check here for authz rather than in the authc path since it's a property of an authenticated user self._check_calling_user_account_state(identity) self._exec_permission_check(subject, permission_list) # Check only after the perms check. Match any allowed permissions that use the namespace as the domain for the authz request non_enabled_domains = self._disabled_domains(permission_list) logger.debug('Found disabled domains in permission set: {}'.format( non_enabled_domains)) # If found domains not enabled and the caller is not a system service or system admin, disallow if non_enabled_domains and identity.user_account_type not in [ AccountTypes.admin, AccountTypes.service ]: logger.debug( 'Failing otherwise passing perm check due to domain state') raise AccountStateError(non_enabled_domains[0]) logger.debug('Passed check permission: {}'.format(permission_list))
def load(self, configuration): with DbAuthorizationHandler._config_lock: conf_path = pkg_resources.resource_filename( anchore_engine.__name__, 'conf/default_yosai_settings.yaml') DbAuthorizationHandler._yosai = Yosai(file_path=conf_path) # Disable sessions, since the APIs are not session-based DbAuthorizationHandler._yosai.security_manager.subject_store.session_storage_evaluator.session_storage_enabled = False
def inner_wrapper(*args, **kwargs): try: with Yosai.context(self._yosai): # Context Manager functions try: try: identity = self.authenticate(request_proxy) if not identity.username: raise UnauthenticatedError('Authentication Required') except: raise UnauthenticatedError('Authentication Required') ApiRequestContextProxy.set_identity(identity) permissions_final = [] # Bind all the permissions as needed for perm in permission_s: domain = perm.domain if perm.domain else '*' action = perm.action if perm.action else '*' target = perm.target if perm.target else '*' if hasattr(domain, 'bind'): domain.bind(operation=f, kwargs=kwargs) domain = domain.value if hasattr(action, 'bind'): action.bind(operation=f, kwargs=kwargs) action = action.value if hasattr(target, 'bind'): target.bind(operation=f, kwargs=kwargs) target = target.value #permissions_final.append(':'.join([domain, action, target])) permissions_final.append(Permission(domain, action, target)) # Do the authz on the bound permissions try: self.authorize(ApiRequestContextProxy().identity(), permissions_final) except UnauthorizedError as ex: raise ex except Exception as e: logger.exception('Error doing authz: {}'.format(e)) raise UnauthorizedError(permissions_final) return f(*args, **kwargs) finally: # Teardown the request context ApiRequestContextProxy.set_identity(None) except UnauthorizedError as ex: return make_response_error(str(ex), in_httpcode=403), 403 except UnauthenticatedError as ex: return Response(response='Unauthorized', status=401, headers=[('WWW-Authenticate', 'basic realm="Authentication required"')]) except AnchoreApiError: raise except Exception as ex: logger.exception('Unexpected exception: {}'.format(ex)) return make_response_error('Internal error', in_httpcode=500), 500
def inner_wrap(*args, **kwargs): newperms = [perm.format(**kwargs) for perm in permission_s] subject = Yosai.get_current_subject() subject.check_permission(newperms, logical_operator) return fn(*args, **kwargs)
def wrap(*args, **kwargs): subject = Yosai.get_current_subject() if not subject.authenticated: msg = "The current Subject is not authenticated. ACCESS DENIED." raise UnauthenticatedException(msg) return fn(*args, **kwargs)
def inline_authz(self, permission_s: list, authc_token: AuthenticationToken=None): """ Non-decorator impl of the @requires() decorator for isolated and inline invocation. Returns authenticated user identity on success or raises an exception :param permission_s: list of Permission objects :param authc_token: optional authc token to use for the authc portion, if omitted or None, the flask request context is used :return: IdentityContext object """ try: with Yosai.context(self._yosai): # Context Manager functions try: try: if not authc_token: identity = self.authenticate(request_proxy) else: identity = self.authenticate_token(authc_token) if not identity.username: raise UnauthenticatedError('Authentication Required') except: raise UnauthenticatedError('Authentication Required') ApiRequestContextProxy.set_identity(identity) permissions_final = [] # Bind all the permissions as needed for perm in permission_s: domain = perm.domain if perm.domain else '*' action = perm.action if perm.action else '*' target = perm.target if perm.target else '*' permissions_final.append(Permission(domain, action, target)) # Do the authz on the bound permissions try: self.authorize(ApiRequestContextProxy.identity(), permissions_final) except UnauthorizedError as ex: raise ex except Exception as e: logger.exception('Error doing authz: {}'.format(e)) raise UnauthorizedError(permissions_final) return ApiRequestContextProxy.identity() finally: # Teardown the request context ApiRequestContextProxy.set_identity(None) except UnauthorizedError as ex: return make_response_error(str(ex), in_httpcode=403), 403 except UnauthenticatedError as ex: return Response(response='Unauthorized', status=401, headers=[('WWW-Authenticate', 'basic realm="Authentication required"')]) except AnchoreApiError: raise except Exception as ex: logger.exception('Unexpected exception: {}'.format(ex)) return make_response_error('Internal error', in_httpcode=500), 500
def test_subject_invalid_single_factor_login( yosai, invalid_walter_username_password_token, event_bus): event_detected = None def event_listener(identifier=None, topic=EVENT_TOPIC): nonlocal event_detected event_detected = identifier event_bus.subscribe(event_listener, 'AUTHENTICATION.FAILED') with Yosai.context(yosai): new_subject = Yosai.get_current_subject() with pytest.raises(IncorrectCredentialsException): new_subject.login(invalid_walter_username_password_token) assert event_detected == invalid_walter_username_password_token.identifier
def invalid_thedude_totp_token(cache_handler, yosai, monkeypatch): keys = cache_handler.keys('*authentication*') for key in keys: cache_handler.cache_region.delete(key) token = int(TOTP(key='AYAGB3C5RPYX5375L5VY2ULKZXMXWLZF', digits=6).generate().token) yield TOTPToken(totp_token=token) keys = cache_handler.keys('*authentication*') for key in keys: cache_handler.cache_region.delete(key) with Yosai.context(yosai): new_subject = Yosai.get_current_subject() da = new_subject.security_manager.authenticator monkeypatch.setattr(da.authc_settings, 'account_lock_threshold', 3) da.init_locking() da.locking_realm.unlock_account('thedude')
def inline_authz(self, permission_s: list): """ Non-decorator impl of the @requires() decorator for isolated and inline invocation. Returns None on success or raises an exception :param permission_s: :return: """ try: with Yosai.context(self._yosai): # Context Manager functions try: try: identity = self.authenticate(request_proxy) if not identity.username: raise UnauthenticatedError( 'Authentication Required') except: raise UnauthenticatedError('Authentication Required') ApiRequestContextProxy.set_identity(identity) permissions_final = [] # Bind all the permissions as needed for perm in permission_s: domain = perm.domain if perm.domain else '*' action = perm.action if perm.action else '*' target = perm.target if perm.target else '*' permissions_final.append(':'.join( [domain, action, target])) # Do the authz on the bound permissions try: self.authorize(identity, permissions_final) except UnauthorizedError as ex: raise ex except Exception as e: logger.exception('Error doing authz: {}'.format(e)) raise UnauthorizedError(permissions_final) return None finally: # Teardown the request context ApiRequestContextProxy.set_identity(None) except UnauthorizedError as ex: return make_response_error(str(ex), in_httpcode=403), 403 except UnauthenticatedError as ex: return Response(response='Unauthorized', status=401, headers=[('WWW-Authenticate', 'basic realm="Authentication required"') ]) except Exception as ex: logger.exception('Unexpected exception: {}'.format(ex)) return make_response_error('Internal error', in_httpcode=500), 500
def test_requires_role_succeeds(yosai, valid_thedude_username_password_token, valid_thedude_totp_token): status = None @Yosai.requires_role(['courier']) def do_something(): nonlocal status status = True with Yosai.context(yosai): new_subject = Yosai.get_current_subject() try: new_subject.login(valid_thedude_username_password_token) except AdditionalAuthenticationRequired: new_subject.login(valid_thedude_totp_token) do_something() assert status new_subject.logout()
def wrap(*args, **kwargs): subject = Yosai.get_current_subject() if subject.identifiers is not None: msg = ("Attempting to perform a guest-only operation. The " "current Subject is NOT a guest (they have either been " "authenticated or remembered from a previous login). " "ACCESS DENIED.") raise UnauthenticatedException(msg) return fn(*args, **kwargs)
def inner_wrap(*args, **kwargs): subject = Yosai.get_current_subject() subject.check_permission(permission_s, logical_operator) return fn(*args, **kwargs)