def test_token_expired(self, user, client, security_service, password_resets, outbox, templates): security_service.send_reset_password_instructions(user) assert len(password_resets) == 1 token = password_resets[0]['token'] r = client.get('security_controller.reset_password', token=token) assert r.status_code == 302 assert r.path == url_for('security_controller.forgot_password') assert len(outbox) == 2 # first email is for the valid reset request assert templates[0].template.name == \ 'security/email/reset_password_instructions.html' assert templates[0].context.get('reset_link') # second email is with a new token assert templates[1].template.name == \ 'security/email/reset_password_instructions.html' assert templates[1].context.get('reset_link') assert templates[0].context.get('reset_link') != \ templates[1].context.get('reset_link') # make sure the user is notified of the new email r = client.follow_redirects(r) assert r.status_code == 200 assert templates[2].template.name == 'security/forgot_password.html' error_msg = 'You did not reset your password within -1 seconds. ' \ f'New instructions have been sent to {user.email}' assert error_msg in r.html
def login(self, remote_app): provider = getattr(self.oauth, remote_app) return provider.authorize( callback=url_for('o_auth_controller.authorized', remote_app=remote_app, _external=True, _scheme='https'))
def test_token_invalid(self, client, templates): r = client.get('security_controller.reset_password', token='fail') assert r.status_code == 302 assert r.path == url_for('security_controller.forgot_password') r = client.follow_redirects(r) assert r.status_code == 200 assert templates[0].template.name == 'security/forgot_password.html' assert 'Invalid reset password token' in r.html
def test_invalid_token(self, client, user, registrations, confirmations, outbox, templates, security_service): security_service.register_user(user) assert len(registrations) == 1 r = client.get( url_for('security_controller.confirm_email', token='fail')) assert r.status_code == 302 assert r.path == url_for('security_controller.send_confirmation_email') assert len(confirmations) == 0 assert len(outbox) == len(templates) == 1 assert templates[0].template.name == 'security/email/welcome.html' assert not user.active assert not user.confirmed_at assert isinstance(current_user._get_current_object(), AnonymousUser)
def test_authed_user_html_request_redirected(self, client): client.login_user() @anonymous_user_required def method(): return None r = method() assert r.status_code == 302 assert r.location == url_for('SECURITY_POST_LOGIN_REDIRECT_ENDPOINT')
def test_http_get_redirects_to_frontend_form(self, user, api_client, security_service, password_resets): security_service.send_reset_password_instructions(user) assert len(password_resets) == 1 token = password_resets[0]['token'] r = api_client.get('security_api.reset_password', token=token) assert r.status_code == 302 assert r.path == url_for( 'SECURITY_API_RESET_PASSWORD_HTTP_GET_REDIRECT', token=token)
def _process_test_client_args(args, kwargs): """ allow calling client.get, client.post, etc methods with an endpoint name. this function forwards the correct kwargs to url_for (as long as they don't conflict with the kwarg names of werkzeug.test.EnvironBuilder, in which case it would be necessary to use `url_for` in the same way as with FlaskClient) """ endpoint_or_url_or_config_key = args and args[0] url_for_kwargs = {} for kwarg_name in (set(kwargs) - ENV_BUILDER_KWARGS): url_for_kwargs[kwarg_name] = kwargs.pop(kwarg_name) url = url_for(endpoint_or_url_or_config_key, **url_for_kwargs) return (url, *args[1:]), kwargs
def register_user(self, user, allow_login=None, send_email=None): """ Service method to register a user. Sends signal `user_registered`. Returns True if the user has been logged in, False otherwise. """ should_login_user = (not self.security.confirmable or self.security.login_without_confirmation) should_login_user = (should_login_user if allow_login is None else allow_login and should_login_user) if should_login_user: user.active = True # confirmation token depends on having user.id set, which requires # the user be committed to the database self.user_manager.save(user, commit=True) confirmation_link, token = None, None if self.security.confirmable: token = self.security_utils_service.generate_confirmation_token( user) confirmation_link = url_for('security_controller.confirm_email', token=token, _external=True) user_registered.send(app._get_current_object(), user=user, confirm_token=token) if (send_email or (send_email is None and app.config.get('SECURITY_SEND_REGISTER_EMAIL'))): self.send_mail( _('flask_unchained.bundles.security:email_subject.register'), to=user.email, template='security/email/welcome.html', user=user, confirmation_link=confirmation_link) if should_login_user: return self.login_user(user) return False
def send_reset_password_instructions(self, user): """ Sends the reset password instructions email for the specified user. Sends signal `reset_password_instructions_sent`. :param user: The user to send the instructions to. """ token = self.security_utils_service.generate_reset_password_token(user) reset_link = url_for('security_controller.reset_password', token=token, _external=True) self.send_mail( _('flask_unchained.bundles.security:email_subject.reset_password_instructions'), to=user.email, template='security/email/reset_password_instructions.html', user=user, reset_link=reset_link) reset_password_instructions_sent.send(app._get_current_object(), user=user, token=token)
def send_email_confirmation_instructions(self, user): """ Sends the confirmation instructions email for the specified user. Sends signal `confirm_instructions_sent`. :param user: The user to send the instructions to. """ token = self.security_utils_service.generate_confirmation_token(user) confirmation_link = url_for('security_controller.confirm_email', token=token, _external=True) self.send_mail( _('flask_unchained.bundles.security:email_subject.email_confirmation_instructions'), to=user.email, template='security/email/email_confirmation_instructions.html', user=user, confirmation_link=confirmation_link) confirm_instructions_sent.send(app._get_current_object(), user=user, token=token)
def test_confirm_email(self, client, registrations, confirmations, user, security_service): security_service.register_user(user) assert len(registrations) == 1 assert user == registrations[0]['user'] assert not user.active assert not user.confirmed_at confirm_token = registrations[0]['confirm_token'] r = client.get( url_for('security_controller.confirm_email', token=confirm_token)) assert r.status_code == 302 assert r.path == '/' assert len(confirmations) == 1 assert user in confirmations assert user.active assert user.confirmed_at assert current_user == user
def test_expired_token(self, client, registrations, confirmations, outbox, templates, user_manager: UserManager, security_service: SecurityService): user = self.register(user_manager, security_service) assert len(registrations) == 1 confirm_token = registrations[0]['confirm_token'] r = client.get('security.confirm_email', token=confirm_token) assert r.status_code == 302 assert r.path == url_for('frontend.resend_confirmation_email') assert len(confirmations) == 0 assert len(outbox) == len(templates) == 2 assert templates[0].template.name == 'security/email/welcome.html' assert templates[ 1].template.name == 'security/email/confirmation_instructions.html' assert templates[1].context.get('confirmation_link') assert not user.active assert not user.confirmed_at assert isinstance(current_user._get_current_object(), AnonymousUser)
def test_confirm_email(self, client, registrations, confirmations, user_manager: UserManager, security_service: SecurityService): user = self.register(user_manager, security_service) assert len(registrations) == 1 assert user == registrations[0]['user'] assert not user.active assert not user.confirmed_at confirm_token = registrations[0]['confirm_token'] r = client.get('security.confirm_email', token=confirm_token) assert r.status_code == 302 assert r.path == url_for('frontend.index') assert r.query == 'welcome' assert len(confirmations) == 1 assert user in confirmations assert user.active assert user.confirmed_at assert current_user == user
def test_token_expired(self, user, api_client, security_service, password_resets, outbox, templates): security_service.send_reset_password_instructions(user) assert len(password_resets) == 1 token = password_resets[0]['token'] r = api_client.get('security_api.reset_password', token=token) assert r.status_code == 302 assert r.path == url_for('SECURITY_EXPIRED_RESET_TOKEN_REDIRECT') assert len(outbox) == len(templates) == 2 # first email is for the valid reset request assert templates[0].template.name == \ 'security/email/reset_password_instructions.html' assert templates[0].context.get('reset_link') # second email is with a new token assert templates[1].template.name == \ 'security/email/reset_password_instructions.html' assert templates[1].context.get('reset_link') assert templates[0].context.get( 'reset_link') != templates[1].context.get('reset_link')
def test_html_login_with_email(self, client, user): r = client.post('admin.login', data=dict(email=user.email, password='******')) assert r.status_code == 302 assert r.path == url_for('admin.index') assert current_user == user
class Config(BundleConfig): ########################################################################## # flask # ########################################################################## DEBUG = get_boolean_env('FLASK_DEBUG', False) FLASH_MESSAGES = False SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'not-secret-key') # FIXME app_dirs = AppDirs('flask-unchained-react-spa') APP_CACHE_FOLDER = app_dirs.user_cache_dir APP_DATA_FOLDER = app_dirs.user_data_dir ADMIN_CATEGORY_ICON_CLASSES = { 'Security': 'glyphicon glyphicon-lock', 'Mail': 'glyphicon glyphicon-envelope', } ########################################################################## # celery # ########################################################################## CELERY_BROKER_URL = 'redis://{host}:{port}/0'.format( host=os.getenv('FLASK_REDIS_HOST', '127.0.0.1'), port=os.getenv('FLASK_REDIS_PORT', 6379), ) CELERY_RESULT_BACKEND = CELERY_BROKER_URL ########################################################################## # mail # ########################################################################## MAIL_ADMINS = ['*****@*****.**'] # FIXME MAIL_DEFAULT_SENDER = ( os.environ.get('FLASK_MAIL_DEFAULT_SENDER_NAME', 'Flask Unchained React SPA'), os.environ.get( 'FLASK_MAIL_DEFAULT_SENDER_EMAIL', f"noreply@{os.environ.get('FLASK_DOMAIN', 'localhost')}")) ########################################################################## # session/cookies # ########################################################################## SESSION_TYPE = 'redis' SESSION_REDIS = redis.Redis( host=os.getenv('FLASK_REDIS_HOST', '127.0.0.1'), port=int(os.getenv('FLASK_REDIS_PORT', 6379)), ) SESSION_PROTECTION = 'strong' SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SECURE = True REMEMBER_COOKIE_HTTPONLY = True # SECURITY_TOKEN_MAX_AGE is fixed from time of token generation; # it does not update on refresh like a session timeout would. for that, # we set (the ironically named) PERMANENT_SESSION_LIFETIME PERMANENT_SESSION_LIFETIME = timedelta(minutes=60) ########################################################################## # security # ########################################################################## SECURITY_PASSWORD_SALT = 'security-password-salt' SECURITY_CONFIRMABLE = True SECURITY_REGISTERABLE = True SECURITY_RECOVERABLE = True SECURITY_CHANGEABLE = True ADMIN_LOGIN_ENDPOINT = 'admin.login' ADMIN_LOGOUT_ENDPOINT = 'admin.logout' SECURITY_POST_LOGIN_REDIRECT_ENDPOINT = 'admin.index' ADMIN_POST_LOGOUT_ENDPOINT = LocalProxy( lambda: url_for('frontend.index', _external=True)) SECURITY_FORGOT_PASSWORD_ENDPOINT = 'frontend.forgot_password' SECURITY_API_RESET_PASSWORD_HTTP_GET_REDIRECT = 'frontend.reset_password' SECURITY_INVALID_RESET_TOKEN_REDIRECT = LocalProxy(lambda: url_for( 'frontend.forgot_password', _external=True) + '?invalid') SECURITY_EXPIRED_RESET_TOKEN_REDIRECT = LocalProxy(lambda: url_for( 'frontend.forgot_password', _external=True) + '?expired') SECURITY_POST_CONFIRM_REDIRECT_ENDPOINT = LocalProxy( lambda: url_for('frontend.index', _external=True) + '?welcome') SECURITY_CONFIRM_ERROR_REDIRECT_ENDPOINT = LocalProxy( lambda: url_for('frontend.resend_confirmation_email', _external=True)) ########################################################################## # database # ########################################################################## SQLALCHEMY_DATABASE_URI = '{engine}://{user}:{pw}@{host}:{port}/{db}'.format( engine=os.getenv('FLASK_DATABASE_ENGINE', 'postgresql+psycopg2'), user=os.getenv('FLASK_DATABASE_USER', 'flask_api'), pw=os.getenv('FLASK_DATABASE_PASSWORD', 'flask_api'), host=os.getenv('FLASK_DATABASE_HOST', '127.0.0.1'), port=os.getenv('FLASK_DATABASE_PORT', 5432), db=os.getenv('FLASK_DATABASE_NAME', 'flask_api'))
class Config(BundleConfig): ########################################################################## # flask # ########################################################################## DEBUG = get_boolean_env('FLASK_DEBUG', False) SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'not-secret-key') # FIXME APP_ROOT = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.abspath(os.path.join(APP_ROOT, os.pardir)) app_dirs = AppDirs('flask-techan-unchained') APP_CACHE_FOLDER = app_dirs.user_cache_dir APP_DATA_FOLDER = app_dirs.user_data_dir os.makedirs(APP_CACHE_FOLDER, exist_ok=True) os.makedirs(APP_DATA_FOLDER, exist_ok=True) FLASH_MESSAGES = False WTF_CSRF_ENABLED = True ########################################################################## # celery # ########################################################################## CELERY_BROKER_URL = 'redis://{host}:{port}/0'.format( host=os.getenv('FLASK_REDIS_HOST', '127.0.0.1'), port=os.getenv('FLASK_REDIS_PORT', 6379), ) CELERY_RESULT_BACKEND = CELERY_BROKER_URL ########################################################################## # mail # ########################################################################## MAIL_ADMINS = ['*****@*****.**'] # FIXME MAIL_DEFAULT_SENDER = ( os.environ.get('FLASK_MAIL_DEFAULT_SENDER_NAME', 'Flask Techan Unchained'), os.environ.get( 'FLASK_MAIL_DEFAULT_SENDER_EMAIL', f"noreply@{os.environ.get('FLASK_DOMAIN', 'localhost')}")) ########################################################################## # session/cookies # ########################################################################## SESSION_TYPE = 'redis' SESSION_REDIS = redis.Redis( host=os.getenv('FLASK_REDIS_HOST', '127.0.0.1'), port=int(os.getenv('FLASK_REDIS_PORT', 6379)), ) SESSION_PROTECTION = 'strong' SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SECURE = True REMEMBER_COOKIE_HTTPONLY = True # SECURITY_TOKEN_MAX_AGE is fixed from time of token generation; # it does not update on refresh like a session timeout would. for that, # we set (the ironically named) PERMANENT_SESSION_LIFETIME PERMANENT_SESSION_LIFETIME = timedelta(minutes=60) ########################################################################## # security # ########################################################################## SECURITY_PASSWORD_SALT = 'security-password-salt' SECURITY_CONFIRMABLE = True SECURITY_REGISTERABLE = True SECURITY_RECOVERABLE = True SECURITY_CHANGEABLE = True SECURITY_FORGOT_PASSWORD_ENDPOINT = 'frontend.forgot_password' SECURITY_API_RESET_PASSWORD_HTTP_GET_REDIRECT = 'frontend.reset_password' SECURITY_INVALID_RESET_TOKEN_REDIRECT = LocalProxy(lambda: url_for( 'frontend.forgot_password', _external=True) + '?invalid') SECURITY_EXPIRED_RESET_TOKEN_REDIRECT = LocalProxy(lambda: url_for( 'frontend.forgot_password', _external=True) + '?expired') SECURITY_POST_CONFIRM_REDIRECT_ENDPOINT = LocalProxy( lambda: url_for('frontend.index', _external=True) + '?welcome') SECURITY_CONFIRM_ERROR_REDIRECT_ENDPOINT = LocalProxy( lambda: url_for('frontend.resend_confirmation_email', _external=True)) ########################################################################## # database # ########################################################################## SQLALCHEMY_DATABASE_URI = '{engine}://{user}:{pw}@{host}:{port}/{db}'.format( engine=os.getenv('FLASK_DATABASE_ENGINE', 'postgresql+psycopg2'), user=os.getenv('FLASK_DATABASE_USER', 'fun_techan'), pw=os.getenv('FLASK_DATABASE_PASSWORD', 'fun_techan'), host=os.getenv('FLASK_DATABASE_HOST', '127.0.0.1'), port=os.getenv('FLASK_DATABASE_PORT', 5432), db=os.getenv('FLASK_DATABASE_NAME', 'fun_techan')) MARKETSTORE_RPC_HOST = os.getenv('MARKETSTORE_RPC_HOST', 'localhost') MARKETSTORE_RPC_PORT = os.getenv('MARKETSTORE_RPC_PORT', '5993') MARKETSTORE_RPC_PATH = os.getenv('MARKETSTORE_RPC_PATH', '/rpc')
def _handle_view(self, name, **kwargs): if not self.is_accessible(): if not user.is_authenticated: return redirect( url_for('ADMIN_LOGIN_ENDPOINT', next=request.url)) abort(HTTPStatus.FORBIDDEN)
def test_html_get(self, client): client.login_user() r = client.get('security_controller.logout') assert r.status_code == 302 assert r.path == url_for('SECURITY_POST_LOGOUT_REDIRECT_ENDPOINT') assert isinstance(current_user._get_current_object(), AnonymousUser)
class Config(AppBundleConfig): ########################################################################## # flask # ########################################################################## DEBUG = get_boolean_env('FLASK_DEBUG', False) SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'change-me-to-a-secret-key!') APP_ROOT = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.abspath(os.path.join(APP_ROOT, os.pardir)) DATA_FOLDER = os.path.join(PROJECT_ROOT, 'data') SSH_FOLDER = os.path.join(PROJECT_ROOT, 'ssh') app_dirs = AppDirs('zillean') APP_CACHE_FOLDER = app_dirs.user_cache_dir APP_DATA_FOLDER = app_dirs.user_data_dir MAX_CONTENT_LENGTH = 2 * 1024 * 1024 * 1024 # SERVER_NAME=os.getenv('FLASK_SERVER_NAME', 'backend:5000') ########################################################################## # celery # ########################################################################## CELERY_BROKER_URL = 'redis://{host}:{port}/0'.format( host=os.getenv('FLASK_REDIS_HOST', '127.0.0.1'), port=os.getenv('FLASK_REDIS_PORT', 6379), ) CELERY_RESULT_BACKEND = CELERY_BROKER_URL ########################################################################## # mail # ########################################################################## MAIL_SERVER = os.environ.get('FLASK_MAIL_HOST', 'localhost') MAIL_PORT = int(os.environ.get('FLASK_MAIL_PORT', 25)) MAIL_USE_TLS = get_boolean_env('FLASK_MAIL_USE_TLS', False) MAIL_USE_SSL = get_boolean_env('FLASK_MAIL_USE_SSL', False) MAIL_USERNAME = os.environ.get('FLASK_MAIL_USERNAME', None) MAIL_PASSWORD = os.environ.get('FLASK_MAIL_PASSWORD', None) MAIL_ADMINS = [os.environ.get('FLASK_MAIL_ADMIN', 'admin@localhost')] MAIL_DEFAULT_SENDER = ( os.environ.get('FLASK_MAIL_DEFAULT_SENDER_NAME', 'Zillean'), os.environ.get('FLASK_MAIL_DEFAULT_SENDER_EMAIL', f"noreply@{os.environ.get('FLASK_DOMAIN', 'zillean.ai')}") ) ########################################################################## # session/cookies # ########################################################################## SESSION_TYPE = 'redis' SESSION_REDIS = redis.Redis( host=os.getenv('FLASK_REDIS_HOST', '127.0.0.1'), port=int(os.getenv('FLASK_REDIS_PORT', 6379)), ) SESSION_PROTECTION = 'strong' SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SECURE = True REMEMBER_COOKIE_HTTPONLY = True # SECURITY_TOKEN_MAX_AGE is fixed from time of token generation; # it does not update on refresh like a session timeout would. for that, # we set (the ironically named) PERMANENT_SESSION_LIFETIME PERMANENT_SESSION_LIFETIME = timedelta(minutes=60) ########################################################################## # security # ########################################################################## FLASH_MESSAGES = False SECURITY_PASSWORD_SALT = os.environ.get('FLASK_PASSWORD_SALT', 'security-password-salt') SECURITY_CONFIRMABLE = True SECURITY_REGISTERABLE = True SECURITY_RECOVERABLE = True SECURITY_CHANGEABLE = True ADMIN_LOGIN_ENDPOINT = 'admin.login' ADMIN_LOGOUT_ENDPOINT = 'admin.logout' ADMIN_POST_LOGOUT_ENDPOINT = LocalProxy( lambda: url_for('frontend.index', _external=True)) SECURITY_FORGOT_PASSWORD_ENDPOINT = 'frontend.forgot_password' SECURITY_API_RESET_PASSWORD_HTTP_GET_REDIRECT = 'frontend.reset_password' SECURITY_INVALID_RESET_TOKEN_REDIRECT = LocalProxy( lambda: url_for('frontend.forgot_password', _external=True) + '?invalid') SECURITY_EXPIRED_RESET_TOKEN_REDIRECT = LocalProxy( lambda: url_for('frontend.forgot_password', _external=True) + '?expired') SECURITY_POST_CONFIRM_REDIRECT_ENDPOINT = LocalProxy( lambda: url_for('frontend.index', _external=True) + '?welcome') SECURITY_CONFIRM_ERROR_REDIRECT_ENDPOINT = LocalProxy( lambda: url_for('frontend.resend_confirmation_email', _external=True)) ########################################################################## # database # ########################################################################## SQLALCHEMY_DATABASE_URI = '{engine}://{user}:{pw}@{host}:{port}/{db}'.format( engine=os.getenv('FLASK_DATABASE_ENGINE', 'postgresql+psycopg2'), user=os.getenv('FLASK_DATABASE_USER', 'flask_api'), pw=os.getenv('FLASK_DATABASE_PASSWORD', 'flask_api'), host=os.getenv('FLASK_DATABASE_HOST', '127.0.0.1'), port=os.getenv('FLASK_DATABASE_PORT', 5432), db=os.getenv('FLASK_DATABASE_NAME', 'flask_api')) WEBPACK_MANIFEST_PATH = os.path.join('static', 'assets', 'manifest.json') OAUTH_REMOTE_APP_GITLAB = dict( consumer_key=os.getenv('OAUTH_GITLAB_CONSUMER_KEY', ''), consumer_secret=os.getenv('OAUTH_GITLAB_CONSUMER_SECRET', ''), base_url='https://gitlab.com/api/v4/user', access_token_url='https://gitlab.com/oauth/token', access_token_method='POST', authorize_url='https://gitlab.com/oauth/authorize', request_token_url=None, request_token_params={'scope': 'openid read_user'}, )
def test_token_invalid(self, api_client): r = api_client.get('security_api.reset_password', token='fail') assert r.status_code == 302 assert r.path == url_for('SECURITY_INVALID_RESET_TOKEN_REDIRECT')