You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by sa...@apache.org on 2016/08/19 23:13:08 UTC

incubator-airflow git commit: [AIRFLOW-444] Add Google authentication backend

Repository: incubator-airflow
Updated Branches:
  refs/heads/master 7a5e1d832 -> df848a556


[AIRFLOW-444] Add Google authentication backend

Add Google authentication backend.
Add Google authentication information to security
docs.

Dear Airflow Maintainers,

Please accept this PR that addresses the following
issues:
-
https://issues.apache.org/jira/browse/AIRFLOW-444

Testing Done:
- Tested Google authentication backend locally
with no issues

This is mostly an adaptation of the GHE
authentication backend.

Closes #1747 from ananya77041/google_auth_backend


Project: http://git-wip-us.apache.org/repos/asf/incubator-airflow/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-airflow/commit/df848a55
Tree: http://git-wip-us.apache.org/repos/asf/incubator-airflow/tree/df848a55
Diff: http://git-wip-us.apache.org/repos/asf/incubator-airflow/diff/df848a55

Branch: refs/heads/master
Commit: df848a5564ed4b2d3281df79f77c51559e95c1a5
Parents: 7a5e1d8
Author: Ananya Mishra <am...@cornell.edu>
Authored: Fri Aug 19 16:12:58 2016 -0700
Committer: Siddharth Anand <si...@yahoo.com>
Committed: Fri Aug 19 16:12:58 2016 -0700

----------------------------------------------------------------------
 airflow/contrib/auth/backends/google_auth.py | 191 ++++++++++++++++++++++
 docs/security.rst                            |  33 ++++
 2 files changed, 224 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/df848a55/airflow/contrib/auth/backends/google_auth.py
----------------------------------------------------------------------
diff --git a/airflow/contrib/auth/backends/google_auth.py b/airflow/contrib/auth/backends/google_auth.py
new file mode 100644
index 0000000..8aed03a
--- /dev/null
+++ b/airflow/contrib/auth/backends/google_auth.py
@@ -0,0 +1,191 @@
+# Copyright 2016 Ananya Mishra (am747@cornell.edu)
+#
+# 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 logging
+
+import flask_login
+
+# Need to expose these downstream
+# pylint: disable=unused-import
+from flask_login import (current_user,
+                         logout_user,
+                         login_required,
+                         login_user)
+# pylint: enable=unused-import
+
+from flask import url_for, redirect, request
+
+from flask_oauthlib.client import OAuth
+
+from airflow import models, configuration, settings
+from airflow.configuration import AirflowConfigException
+
+_log = logging.getLogger(__name__)
+
+
+def get_config_param(param):
+    return str(configuration.get('google', param))
+
+
+class GoogleUser(models.User):
+
+    def __init__(self, user):
+        self.user = user
+
+    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 self.user.get_id()
+
+    def data_profiling(self):
+        '''Provides access to data profiling tools'''
+        return True
+
+    def is_superuser(self):
+        '''Access all the things'''
+        return True
+
+
+class AuthenticationError(Exception):
+    pass
+
+
+class GoogleAuthBackend(object):
+
+    def __init__(self):
+        # self.google_host = get_config_param('host')
+        self.login_manager = flask_login.LoginManager()
+        self.login_manager.login_view = 'airflow.login'
+        self.flask_app = None
+        self.google_oauth = None
+        self.api_rev = None
+
+    def init_app(self, flask_app):
+        self.flask_app = flask_app
+
+        self.login_manager.init_app(self.flask_app)
+
+        self.google_oauth = OAuth(self.flask_app).remote_app(
+            'google',
+            consumer_key=get_config_param('client_id'),
+            consumer_secret=get_config_param('client_secret'),
+            request_token_params={'scope': '''https://www.googleapis.com/auth/userinfo.profile
+                                        https://www.googleapis.com/auth/userinfo.email'''},
+            base_url='https://www.google.com/accounts/',
+            request_token_url=None,
+            access_token_method='POST',
+            access_token_url='https://accounts.google.com/o/oauth2/token',
+            authorize_url='https://accounts.google.com/o/oauth2/auth')
+
+        self.login_manager.user_loader(self.load_user)
+
+        self.flask_app.add_url_rule(get_config_param('oauth_callback_route'),
+                                    'google_oauth_callback',
+                                    self.oauth_callback)
+
+    def login(self, request):
+        _log.debug('Redirecting user to Google login')
+        return self.google_oauth.authorize(callback=url_for(
+            'google_oauth_callback',
+            _external=True,
+            next=request.args.get('next') or request.referrer or None))
+
+    def get_google_user_profile_info(self, google_token):
+        resp = self.google_oauth.get('https://www.googleapis.com/oauth2/v1/userinfo',
+                                    token=(google_token, ''))
+
+        if not resp or resp.status != 200:
+            raise AuthenticationError(
+                'Failed to fetch user profile, status ({0})'.format(
+                    resp.status if resp else 'None'))
+
+        return resp.data['name'], resp.data['email']
+
+    def domain_check(self, email):
+        domain = email.split('@')[1]
+        if domain == get_config_param('domain'):
+            return True
+        return False
+
+    def load_user(self, userid):
+        if not userid or userid == 'None':
+            return None
+
+        session = settings.Session()
+        user = session.query(models.User).filter(
+            models.User.id == int(userid)).first()
+        session.expunge_all()
+        session.commit()
+        session.close()
+        return GoogleUser(user)
+
+    def oauth_callback(self):
+        _log.debug('Google OAuth callback called')
+
+        next_url = request.args.get('next') or url_for('admin.index')
+
+        resp = self.google_oauth.authorized_response()
+
+        try:
+            if resp is None:
+                raise AuthenticationError(
+                    'Null response from Google, denying access.'
+                )
+
+            google_token = resp['access_token']
+
+            username, email = self.get_google_user_profile_info(google_token)
+
+            if not self.domain_check(email):
+                return redirect(url_for('airflow.noaccess'))
+
+        except AuthenticationError:
+            _log.exception('')
+            return redirect(url_for('airflow.noaccess'))
+
+        session = settings.Session()
+
+        user = session.query(models.User).filter(
+            models.User.username == username).first()
+
+        if not user:
+            user = models.User(
+                username=username,
+                email=email,
+                is_superuser=False)
+
+        session.merge(user)
+        session.commit()
+        login_user(GoogleUser(user))
+        session.commit()
+        session.close()
+
+        return redirect(next_url)
+
+login_manager = GoogleAuthBackend()
+
+
+def login(self, request):
+    return login_manager.login(request)
+

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/df848a55/docs/security.rst
----------------------------------------------------------------------
diff --git a/docs/security.rst b/docs/security.rst
index 50d9167..b8f13ca 100644
--- a/docs/security.rst
+++ b/docs/security.rst
@@ -251,3 +251,36 @@ backend. In order to setup an application:
 5. Fill in the required information (the 'Authorization callback URL' must be fully qualifed e.g. http://airflow.example.com/example/ghe_oauth/callback)
 6. Click 'Register application'
 7. Copy 'Client ID', 'Client Secret', and your callback route to your airflow.cfg according to the above example
+
+Google Authentication
+''''''''''''''''''''''''''''''''''''''
+
+The Google authentication backend can be used to authenticate users
+against Google using OAuth2. You must specify a domain to restrict login
+to only members of that domain.
+
+.. code-block:: bash
+
+    [webserver]
+    authenticate = True
+    auth_backend = airflow.contrib.auth.backends.google_auth
+
+    [google]
+    client_id = google_client_id
+    client_secret = google_client_secret
+    oauth_callback_route = /oauth2callback
+    domain = example.com
+
+Setting up Google Authentication
+'''''''''''''''''''''''''''''
+
+An application must be setup in the Google API Console before you can use the Google authentication
+backend. In order to setup an application:
+
+1. Navigate to https://console.developers.google.com/apis/
+2. Select 'Credentials' from the left hand nav
+3. Click 'Create credentials' and choose 'OAuth client ID'
+4. Choose 'Web application'
+5. Fill in the required information (the 'Authorized redirect URIs' must be fully qualifed e.g. http://airflow.example.com/oauth2callback)
+6. Click 'Create'
+7. Copy 'Client ID', 'Client Secret', and your redirect URI to your airflow.cfg according to the above example