Repository: incubator-airflow Updated Branches: refs/heads/master b6f9ba9cc -> 7cba83333
[AIRFLOW-2181] Convert password_auth and test_password_endpoints from DOS to UNIX Closes #3102 from dan-sf/AIRFLOW-2181 Project: http://git-wip-us.apache.org/repos/asf/incubator-airflow/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-airflow/commit/7cba8333 Tree: http://git-wip-us.apache.org/repos/asf/incubator-airflow/tree/7cba8333 Diff: http://git-wip-us.apache.org/repos/asf/incubator-airflow/diff/7cba8333 Branch: refs/heads/master Commit: 7cba83333c5227ce37967c65d189a5e994898c68 Parents: b6f9ba9 Author: dan-sf <fowler...@gmail.com> Authored: Wed Mar 7 09:49:48 2018 +0100 Committer: Fokko Driesprong <fokkodriespr...@godatadriven.com> Committed: Wed Mar 7 09:49:48 2018 +0100 ---------------------------------------------------------------------- airflow/contrib/auth/backends/password_auth.py | 418 +++++++++---------- .../api/experimental/test_password_endpoints.py | 162 +++---- 2 files changed, 290 insertions(+), 290 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/7cba8333/airflow/contrib/auth/backends/password_auth.py ---------------------------------------------------------------------- diff --git a/airflow/contrib/auth/backends/password_auth.py b/airflow/contrib/auth/backends/password_auth.py index 33f3ae0..cfb921c 100644 --- a/airflow/contrib/auth/backends/password_auth.py +++ b/airflow/contrib/auth/backends/password_auth.py @@ -1,209 +1,209 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import unicode_literals - -from sys import version_info - -import base64 -import flask_login -from flask_login import current_user -from flask import flash, Response -from wtforms import Form, PasswordField, StringField -from wtforms.validators import InputRequired -from functools import wraps - -from flask import url_for, redirect, make_response -from flask_bcrypt import generate_password_hash, check_password_hash - -from sqlalchemy import Column, String -from sqlalchemy.ext.hybrid import hybrid_property - -from airflow import settings -from airflow import models -from airflow.utils.db import provide_session -from airflow.utils.log.logging_mixin import LoggingMixin - -login_manager = flask_login.LoginManager() -login_manager.login_view = 'airflow.login' # Calls login() below -login_manager.login_message = None - -log = LoggingMixin().log -PY3 = version_info[0] == 3 - - -class AuthenticationError(Exception): - pass - - -class PasswordUser(models.User): - _password = Column('password', String(255)) - - def __init__(self, user): - self.user = user - - @hybrid_property - def password(self): - return self._password - - @password.setter - def _set_password(self, plaintext): - self._password = generate_password_hash(plaintext, 12) - if PY3: - self._password = str(self._password, 'utf-8') - - def authenticate(self, plaintext): - return check_password_hash(self._password, plaintext) - - def is_active(self): - '''Required by flask_login''' - return True - - def is_authenticated(self): - '''Required by flask_login''' - return True - - def is_anonymous(self): - '''Required by flask_login''' - return False - - def get_id(self): - '''Returns the current user id as required by flask_login''' - return str(self.id) - - def data_profiling(self): - '''Provides access to data profiling tools''' - return True - - def is_superuser(self): - '''Access all the things''' - return True - - -@login_manager.user_loader -@provide_session -def load_user(userid, session=None): - log.debug("Loading user %s", userid) - if not userid or userid == 'None': - return None - - user = session.query(models.User).filter(models.User.id == int(userid)).first() - return PasswordUser(user) - - -def authenticate(session, username, password): - """ - Authenticate a PasswordUser with the specified - username/password. - - :param session: An active SQLAlchemy session - :param username: The username - :param password: The password - - :raise AuthenticationError: if an error occurred - :return: a PasswordUser - """ - if not username or not password: - raise AuthenticationError() - - user = session.query(PasswordUser).filter( - PasswordUser.username == username).first() - - if not user: - raise AuthenticationError() - - if not user.authenticate(password): - raise AuthenticationError() - - log.info("User %s successfully authenticated", username) - return user - - -@provide_session -def login(self, request, session=None): - if current_user.is_authenticated(): - flash("You are already logged in") - return redirect(url_for('admin.index')) - - username = None - password = None - - form = LoginForm(request.form) - - if request.method == 'POST' and form.validate(): - username = request.form.get("username") - password = request.form.get("password") - - try: - user = authenticate(session, username, password) - flask_login.login_user(user) - - return redirect(request.args.get("next") or url_for("admin.index")) - except AuthenticationError: - flash("Incorrect login details") - return self.render('airflow/login.html', - title="Airflow - Login", - form=form) - finally: - session.commit() - session.close() - - -class LoginForm(Form): - username = StringField('Username', [InputRequired()]) - password = PasswordField('Password', [InputRequired()]) - - -def _unauthorized(): - """ - Indicate that authorization is required - :return: - """ - return Response("Unauthorized", 401, {"WWW-Authenticate": "Basic"}) - - -def _forbidden(): - return Response("Forbidden", 403) - - -def init_app(app): - pass - - -def requires_authentication(function): - @wraps(function) - def decorated(*args, **kwargs): - from flask import request - - header = request.headers.get("Authorization") - if header: - userpass = ''.join(header.split()[1:]) - username, password = base64.b64decode(userpass).decode("utf-8").split(":", 1) - - session = settings.Session() - try: - authenticate(session, username, password) - - response = function(*args, **kwargs) - response = make_response(response) - return response - - except AuthenticationError: - return _forbidden() - - finally: - session.commit() - session.close() - return _unauthorized() - return decorated +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import unicode_literals + +from sys import version_info + +import base64 +import flask_login +from flask_login import current_user +from flask import flash, Response +from wtforms import Form, PasswordField, StringField +from wtforms.validators import InputRequired +from functools import wraps + +from flask import url_for, redirect, make_response +from flask_bcrypt import generate_password_hash, check_password_hash + +from sqlalchemy import Column, String +from sqlalchemy.ext.hybrid import hybrid_property + +from airflow import settings +from airflow import models +from airflow.utils.db import provide_session +from airflow.utils.log.logging_mixin import LoggingMixin + +login_manager = flask_login.LoginManager() +login_manager.login_view = 'airflow.login' # Calls login() below +login_manager.login_message = None + +log = LoggingMixin().log +PY3 = version_info[0] == 3 + + +class AuthenticationError(Exception): + pass + + +class PasswordUser(models.User): + _password = Column('password', String(255)) + + def __init__(self, user): + self.user = user + + @hybrid_property + def password(self): + return self._password + + @password.setter + def _set_password(self, plaintext): + self._password = generate_password_hash(plaintext, 12) + if PY3: + self._password = str(self._password, 'utf-8') + + def authenticate(self, plaintext): + return check_password_hash(self._password, plaintext) + + def is_active(self): + '''Required by flask_login''' + return True + + def is_authenticated(self): + '''Required by flask_login''' + return True + + def is_anonymous(self): + '''Required by flask_login''' + return False + + def get_id(self): + '''Returns the current user id as required by flask_login''' + return str(self.id) + + def data_profiling(self): + '''Provides access to data profiling tools''' + return True + + def is_superuser(self): + '''Access all the things''' + return True + + +@login_manager.user_loader +@provide_session +def load_user(userid, session=None): + log.debug("Loading user %s", userid) + if not userid or userid == 'None': + return None + + user = session.query(models.User).filter(models.User.id == int(userid)).first() + return PasswordUser(user) + + +def authenticate(session, username, password): + """ + Authenticate a PasswordUser with the specified + username/password. + + :param session: An active SQLAlchemy session + :param username: The username + :param password: The password + + :raise AuthenticationError: if an error occurred + :return: a PasswordUser + """ + if not username or not password: + raise AuthenticationError() + + user = session.query(PasswordUser).filter( + PasswordUser.username == username).first() + + if not user: + raise AuthenticationError() + + if not user.authenticate(password): + raise AuthenticationError() + + log.info("User %s successfully authenticated", username) + return user + + +@provide_session +def login(self, request, session=None): + if current_user.is_authenticated(): + flash("You are already logged in") + return redirect(url_for('admin.index')) + + username = None + password = None + + form = LoginForm(request.form) + + if request.method == 'POST' and form.validate(): + username = request.form.get("username") + password = request.form.get("password") + + try: + user = authenticate(session, username, password) + flask_login.login_user(user) + + return redirect(request.args.get("next") or url_for("admin.index")) + except AuthenticationError: + flash("Incorrect login details") + return self.render('airflow/login.html', + title="Airflow - Login", + form=form) + finally: + session.commit() + session.close() + + +class LoginForm(Form): + username = StringField('Username', [InputRequired()]) + password = PasswordField('Password', [InputRequired()]) + + +def _unauthorized(): + """ + Indicate that authorization is required + :return: + """ + return Response("Unauthorized", 401, {"WWW-Authenticate": "Basic"}) + + +def _forbidden(): + return Response("Forbidden", 403) + + +def init_app(app): + pass + + +def requires_authentication(function): + @wraps(function) + def decorated(*args, **kwargs): + from flask import request + + header = request.headers.get("Authorization") + if header: + userpass = ''.join(header.split()[1:]) + username, password = base64.b64decode(userpass).decode("utf-8").split(":", 1) + + session = settings.Session() + try: + authenticate(session, username, password) + + response = function(*args, **kwargs) + response = make_response(response) + return response + + except AuthenticationError: + return _forbidden() + + finally: + session.commit() + session.close() + return _unauthorized() + return decorated http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/7cba8333/tests/www/api/experimental/test_password_endpoints.py ---------------------------------------------------------------------- diff --git a/tests/www/api/experimental/test_password_endpoints.py b/tests/www/api/experimental/test_password_endpoints.py index 2c2cc7f..1bf58fa 100644 --- a/tests/www/api/experimental/test_password_endpoints.py +++ b/tests/www/api/experimental/test_password_endpoints.py @@ -1,81 +1,81 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import unittest - -from datetime import datetime - -from airflow import models -from airflow import configuration -from airflow.www import app as application -from airflow.settings import Session -from airflow.contrib.auth.backends.password_auth import PasswordUser - -try: - from ConfigParser import DuplicateSectionError -except ImportError: - from configparser import DuplicateSectionError - - -class ApiPasswordTests(unittest.TestCase): - def setUp(self): - configuration.load_test_config() - try: - configuration.conf.add_section("api") - except DuplicateSectionError: - pass - - configuration.conf.set("api", - "auth_backend", - "airflow.contrib.auth.backends.password_auth") - - self.app = application.create_app(testing=True) - - session = Session() - user = models.User() - password_user = PasswordUser(user) - password_user.username = 'hello' - password_user.password = 'world' - session.add(password_user) - session.commit() - session.close() - - def test_authorized(self): - with self.app.test_client() as c: - url_template = '/api/experimental/dags/{}/dag_runs' - response = c.post( - url_template.format('example_bash_operator'), - data=json.dumps(dict(run_id='my_run' + datetime.now().isoformat())), - content_type="application/json", - headers={'Authorization': 'Basic aGVsbG86d29ybGQ='} # hello:world - ) - self.assertEqual(200, response.status_code) - - def test_unauthorized(self): - with self.app.test_client() as c: - url_template = '/api/experimental/dags/{}/dag_runs' - response = c.post( - url_template.format('example_bash_operator'), - data=json.dumps(dict(run_id='my_run' + datetime.now().isoformat())), - content_type="application/json" - ) - - self.assertEqual(401, response.status_code) - - def tearDown(self): - session = Session() - session.query(models.User).delete() - session.commit() - session.close() +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import unittest + +from datetime import datetime + +from airflow import models +from airflow import configuration +from airflow.www import app as application +from airflow.settings import Session +from airflow.contrib.auth.backends.password_auth import PasswordUser + +try: + from ConfigParser import DuplicateSectionError +except ImportError: + from configparser import DuplicateSectionError + + +class ApiPasswordTests(unittest.TestCase): + def setUp(self): + configuration.load_test_config() + try: + configuration.conf.add_section("api") + except DuplicateSectionError: + pass + + configuration.conf.set("api", + "auth_backend", + "airflow.contrib.auth.backends.password_auth") + + self.app = application.create_app(testing=True) + + session = Session() + user = models.User() + password_user = PasswordUser(user) + password_user.username = 'hello' + password_user.password = 'world' + session.add(password_user) + session.commit() + session.close() + + def test_authorized(self): + with self.app.test_client() as c: + url_template = '/api/experimental/dags/{}/dag_runs' + response = c.post( + url_template.format('example_bash_operator'), + data=json.dumps(dict(run_id='my_run' + datetime.now().isoformat())), + content_type="application/json", + headers={'Authorization': 'Basic aGVsbG86d29ybGQ='} # hello:world + ) + self.assertEqual(200, response.status_code) + + def test_unauthorized(self): + with self.app.test_client() as c: + url_template = '/api/experimental/dags/{}/dag_runs' + response = c.post( + url_template.format('example_bash_operator'), + data=json.dumps(dict(run_id='my_run' + datetime.now().isoformat())), + content_type="application/json" + ) + + self.assertEqual(401, response.status_code) + + def tearDown(self): + session = Session() + session.query(models.User).delete() + session.commit() + session.close()