瀏覽代碼

[api] Skeleton of custom JWT authentication

Not enabled by default as currently insecure.

[desktop]
[[auth]]
-- Multiple Authentication backends are supported by specifying a comma-separated list in order of priority.
api_auth=desktop.auth.api_authentications.JwtAuthentication
Romain Rigaux 4 年之前
父節點
當前提交
8c596bcd2a

+ 4 - 0
desktop/conf.dist/hue.ini

@@ -358,6 +358,10 @@
     # Multiple Authentication backend combinations are supported by specifying a comma-separated list in order of priority.
     ## backend=desktop.auth.backend.AllowFirstUserDjangoBackend
 
+    # Custom Authentication backends for the REST API.
+    # Multiple Authentication backends are supported by specifying a comma-separated list in order of priority.
+    ## api_auth=rest_framework_simplejwt.authentication.JWTAuthentication,rest_framework.authentication.SessionAuthentication
+
     # Class which defines extra accessor methods for User objects.
     ## user_aug=desktop.auth.backend.DefaultUserAugmentor
 

+ 4 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -362,6 +362,10 @@
     # Multiple Authentication backend combinations are supported by specifying a comma-separated list in order of priority.
     ## backend=desktop.auth.backend.AllowFirstUserDjangoBackend
 
+    # Custom Authentication backends for the REST API.
+    # Multiple Authentication backends are supported by specifying a comma-separated list in order of priority.
+    ## api_auth=rest_framework_simplejwt.authentication.JWTAuthentication,rest_framework.authentication.SessionAuthentication
+
     # Class which defines extra accessor methods for User objects.
     ## user_aug=desktop.auth.backend.DefaultUserAugmentor
 

+ 67 - 0
desktop/core/src/desktop/auth/api_authentications.py

@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you 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
+
+from rest_framework import authentication
+from rest_framework import exceptions
+
+from useradmin.models import User
+
+LOG = logging.getLogger(__name__)
+
+
+class JwtAuthentication(authentication.BaseAuthentication):
+
+  def authenticate(self, request):
+    authorization_header = request.META.get('HTTP_AUTHORIZATION')
+    if not authorization_header:
+      LOG.debug('JwtAuthentication: no authorization header')
+      return None
+
+    bearer = authorization_header[len('Bearer '):]
+    if not bearer:
+      LOG.debug('JwtAuthentication: no Bearer value')
+      return None
+
+    LOG.debug('JwtAuthentication: got token %s' % bearer)
+
+    # Decode token via jwt module
+    # check expiration, get userId, handle errors
+
+    # token = '...'
+
+    # cf. below similar to backend.py
+    # user = find_or_create_user(
+    #   username,
+    #   password,
+    #   is_superuser=False
+    # )
+    user = User.objects.get(username='test')
+
+    # ensure_has_a_group(user)
+    # user = rewrite_user(user)
+
+    # We should persist the token (to reuse for communicating with external services as the user, e.g. Impala)
+    # either via a new DB field (might just be cleaner, even if requires a DB migration) or in the json blob.
+    # if ENABLE_ORGANIZATIONS.get():
+    #   user.token = token
+    # else:
+    #   user.profile.update_data({'token': token})
+    #   user.profile.save()
+
+    return (user, None)

+ 51 - 0
desktop/core/src/desktop/auth/api_authentications_tests.py

@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you 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 sys
+
+from nose.tools import assert_equal, assert_true
+
+from desktop.auth.backend import rewrite_user
+from desktop.auth.api_authentications import JwtAuthentication
+from desktop.lib.django_test_util import make_logged_in_client
+from useradmin.models import User
+
+if sys.version_info[0] > 2:
+  from unittest.mock import patch, Mock, MagicMock
+else:
+  from mock import patch, Mock, MagicMock
+
+
+class TestJwtAuthentication():
+  def setUp(self):
+    self.client = make_logged_in_client(username="test", groupname="default", recreate=True, is_superuser=False)
+    self.user = rewrite_user(User.objects.get(username="test"))
+
+  def test_authenticate(self):
+    HEADERS = {
+      "Authorization":
+      "Bearer eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsid29ya2xvYWQtYXBwIiwicmFuZ2VyIl0sImV4cCI6MTYyNjI1Njg5MywiaWF0IjoxNjI2MjU2NTkzLCJpc3MiOiJDbG91ZGVyYTEiLCJqdGkiOiJpZDEiLCJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJ1c2VyIjoidGVzdF91c2VyIn0.jvyVDxbWTAik0jbdUcIc9ZANNrJZUCWH-Pg7FloRhg0ZYAETd_AO3p5v_ppoMmVcPD2xBSrngA5J3_A_zPBvQ_hdDlpb0_-mCCJfGhC5tju4bI9EE9Akdn2FrrsqrvQQ8cPyGsIlvoIxrK1De4f74MmUaxfN7Hrrcue1PTY4u4IB9cWQqV9vIcX99Od5PUaNekLIee-I8gweqvfGEEsW7qWUM63nh59_TOB3LLq-YcEuaX1h_oiTATeCssjk_ee9RrJGLNyKmC0WJ4UrEWn8a_T3bwCy8CMe0zV5PSuuvPHy0FvnTo2il5SDjGimxKcbpgNiJdfblslu6i35DlfiWg"
+    }
+    request = MagicMock(HEADERS=HEADERS)
+
+    user, token = JwtAuthentication().authenticate(request=request)
+
+    assert_equal(user, self.user)
+    assert_true(user.is_authenticated)
+
+  # TODO:
+  # check user.token, test new user, existing user, failure to authenticate

+ 10 - 0
desktop/core/src/desktop/conf.py

@@ -1078,6 +1078,16 @@ AUTH = ConfigSection(
                         "desktop.auth.backend.AllowAllBackend (allows everyone), " +
                         "desktop.auth.backend.AllowFirstUserDjangoBackend (relies on Django and user manager, after the first login). " +
                         "Multiple Authentication backends are supported by specifying a comma-separated list in order of priority.")),
+    API_AUTH=Config(
+        "api_auth",
+        default=[
+          'rest_framework_simplejwt.authentication.JWTAuthentication',
+          'rest_framework.authentication.SessionAuthentication'],
+        type=coerce_csv,
+        help=_(
+          "Custom Authentication backends for the REST API." +
+          "Multiple Authentication backends are supported by specifying a comma-separated list in order of priority.")
+    ),
     USER_AUGMENTOR=Config("user_augmentor",
                    default="desktop.auth.backend.DefaultUserAugmentor",
                    help=_("Class which defines extra accessor methods for User objects.")),

+ 16 - 20
desktop/core/src/desktop/settings.py

@@ -282,26 +282,6 @@ PYLINTRC = get_run_root('.pylintrc')
 # Custom CSRF Failure View
 CSRF_FAILURE_VIEW = 'desktop.views.csrf_failure'
 
-REST_FRAMEWORK = {
-    'DEFAULT_PERMISSION_CLASSES': [
-      'rest_framework.permissions.IsAuthenticated',
-    ],
-    'DEFAULT_AUTHENTICATION_CLASSES': (
-      'rest_framework_simplejwt.authentication.JWTAuthentication',
-      'rest_framework.authentication.SessionAuthentication',
-    ),
-}
-
-SIMPLE_JWT = {
-  'ACCESS_TOKEN_LIFETIME': datetime.timedelta(days=1),
-  'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=7),
-  'ROTATE_REFRESH_TOKENS': False,
-  'BLACKLIST_AFTER_ROTATION': True,
-  'UPDATE_LAST_LOGIN': False,
-  'AUTH_HEADER_TYPES': ('Bearer',),
-}
-
-
 ############################################################
 # Part 4: Installation of apps
 ############################################################
@@ -350,6 +330,22 @@ if DEBUG: # For simplification, force all DEBUG when django_debug_mode is True a
 # configs.
 ############################################################
 
+REST_FRAMEWORK = {
+    'DEFAULT_PERMISSION_CLASSES': [
+      'rest_framework.permissions.IsAuthenticated',
+    ],
+    'DEFAULT_AUTHENTICATION_CLASSES': desktop.conf.AUTH.API_AUTH.get()
+}
+
+SIMPLE_JWT = {
+  'ACCESS_TOKEN_LIFETIME': datetime.timedelta(days=1),
+  'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=7),
+  'ROTATE_REFRESH_TOKENS': False,
+  'BLACKLIST_AFTER_ROTATION': True,
+  'UPDATE_LAST_LOGIN': False,
+  'AUTH_HEADER_TYPES': ('Bearer',),
+}
+
 if desktop.conf.ENABLE_ORGANIZATIONS.get():
   AUTH_USER_MODEL = 'useradmin.OrganizationUser'
   MIGRATION_MODULES = {