浏览代码

[notebook] Add ability to authorize Github and provide authenticated access

Jenny Kim 10 年之前
父节点
当前提交
7009b3e

+ 7 - 2
desktop/conf/pseudo-distributed.ini.tmpl

@@ -600,12 +600,17 @@
   ## Main flag to override the automatic starting of the DBProxy server.
   # enable_dbproxy_server=true
 
-  ## Base URL to Remote Github Server
+  ## Base URL to Remote GitHub Server
   # github_remote_url=https://github.com
 
-  ## Base URL to Github API
+  ## Base URL to GitHub API
   # github_api_url=https://api.github.com
 
+  ## Client ID for Authorized GitHub Application
+  # github_client_id=
+
+  ## Client Secret for Authorized GitHub Application
+  # github_client_secret=
 
 ###########################################################################
 # Settings to configure your Hadoop cluster.

+ 37 - 5
desktop/libs/notebook/src/notebook/api.py

@@ -18,7 +18,7 @@
 import json
 import logging
 
-from django.http import HttpResponseBadRequest
+from django.http import HttpResponseBadRequest, HttpResponseRedirect
 from django.utils.translation import ugettext as _
 from django.views.decorators.http import require_GET, require_POST
 
@@ -305,17 +305,49 @@ def autocomplete(request, database=None, table=None, column=None, nested=None):
 def github_fetch(request):
   response = {'status': -1}
 
-  api = GithubClient()
+  api = GithubClient(access_token=request.session.get('github_access_token'))
 
   response['url'] = url = request.GET.get('url')
 
   if url:
     owner, repo, branch, filepath = api.parse_github_url(url)
-
-    response['status'] = 0
     content = api.get_file_contents(owner, repo, filepath, branch)
-    response['content'] = json.loads(content)
+    try:
+      response['content'] = json.loads(content)
+    except ValueError:
+      # Content is not JSON-encoded so return plain-text
+      response['content'] = content
+    response['status'] = 0
   else:
     return HttpResponseBadRequest(_('url param is required'))
 
   return JsonResponse(response)
+
+
+@api_error_handler
+def github_authorize(request):
+  access_token = request.session.get('github_access_token')
+  if access_token and GithubClient.is_authenticated(access_token):
+    response = {
+      'status': 0,
+      'message': _('User is already authenticated to GitHub.')
+    }
+    return JsonResponse(response)
+  else:
+    auth_url = GithubClient.get_authorization_url()
+    return HttpResponseRedirect(auth_url)
+
+
+@api_error_handler
+def github_callback(request):
+  response = {'status': -1}
+
+  if 'code' in request.GET:
+    session_code = request.GET.get('code')
+    request.session['github_access_token'] = GithubClient.get_access_token(session_code)
+    response['status'] = 0
+    response['message'] = _('User successfully authenticated to GitHub.')
+  else:
+    response['message'] = _('Could not decode file content to JSON.')
+
+  return JsonResponse(response)

+ 18 - 6
desktop/libs/notebook/src/notebook/conf.py

@@ -65,18 +65,30 @@ ENABLE_DBPROXY_SERVER = Config(
   type=bool,
   default=True)
 
-
 GITHUB_REMOTE_URL = Config(
-    "github_remote_url",
-    help="Base URL to Github Remote Server",
+    key="github_remote_url",
+    help="Base URL to GitHub Remote Server",
     default='https://github.com',
     type=coerce_string,
 )
 
-
 GITHUB_API_URL = Config(
-    "github_api_url",
-    help="Base URL to Github API",
+    key="github_api_url",
+    help="Base URL to GitHub API",
     default='https://api.github.com',
     type=coerce_string,
 )
+
+GITHUB_CLIENT_ID = Config(
+    key="github_client_id",
+    help=_t("The Client ID of the GitHub application."),
+    type=coerce_string,
+    default=""
+)
+
+GITHUB_CLIENT_SECRET = Config(
+    key="github_client_secret",
+    help=_t("The Client Secret of the GitHub application."),
+    type=coerce_string,
+    default=""
+)

+ 77 - 14
desktop/libs/notebook/src/notebook/github.py

@@ -25,7 +25,7 @@ import urllib
 from desktop.lib.rest.http_client import HttpClient, RestException
 from desktop.lib.rest import resource
 
-from notebook.conf import GITHUB_REMOTE_URL, GITHUB_API_URL
+from notebook.conf import GITHUB_REMOTE_URL, GITHUB_API_URL, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
 
 
 LOG = logging.getLogger(__name__)
@@ -45,37 +45,100 @@ class GithubClient(object):
   BRANCH_RE = "(?P<branch>[\w\.@\:\-~]+)"
   FILEPATH_RE = "(?P<filepath>.+)"
 
+  DEFAULT_SCOPES = ['repo', 'user']
+
 
   def __init__(self, **options):
-    self._github_base_url = options.get('github_remote_url', GITHUB_REMOTE_URL.get()).strip('/')
-    self._api_url = options.get('github_api_url', GITHUB_API_URL.get()).strip('/')
+    self._github_base_url = options.get('remote_url', GITHUB_REMOTE_URL.get()).strip('/')
+    self._api_url = options.get('api_url', GITHUB_API_URL.get()).strip('/')
 
     self._client = HttpClient(self._api_url, logger=LOG)
     self._root = resource.Resource(self._client)
+
     self.__headers = {}
+    access_token = options.get('access_token')
+    if access_token:
+      self.__headers['Authorization'] = 'token %s' % access_token
+      # TODO: Redact access_token from logs
     self.__params = ()
 
 
-  @property
-  def github_url_regex(self):
-    return re.compile('%s/%s/%s/blob/%s/%s' % (self._github_base_url, self.OWNER_RE, self.REPO_RE, self.BRANCH_RE, self.FILEPATH_RE))
+  @classmethod
+  def get_authorization_url(cls, **options):
+    """
+    https://developer.github.com/guides/basics-of-authentication/
+    """
+    remote_url = options.get('remote_url', GITHUB_REMOTE_URL.get()).strip('/')
+    client_id = options.get('client_id', GITHUB_CLIENT_ID.get())
+    scopes_list = options.get('scopes_list', cls.DEFAULT_SCOPES)
+    scopes = ','.join(scopes_list)
+    return '%s/login/oauth/authorize?scope=%s&client_id=%s' % (remote_url, scopes, client_id)
 
 
-  def _clean_path(self, filepath):
-    cleaned_path = filepath.strip('/')
-    cleaned_path = urllib.unquote(cleaned_path)
-    return cleaned_path
+  @classmethod
+  def get_access_token(cls, session_code, **options):
+    remote_url = options.get('remote_url', GITHUB_REMOTE_URL.get()).strip('/')
+    client_id = options.get('client_id', GITHUB_CLIENT_ID.get())
+    client_secret = options.get('client_secret', GITHUB_CLIENT_SECRET.get())
+
+    try:
+      client = HttpClient(remote_url, logger=LOG)
+      root = resource.Resource(client)
+      data = {
+        'client_id': client_id,
+        'client_secret': client_secret,
+        'code': session_code
+      }
+      headers = {
+        'content-type':'application/json',
+        'Accept': 'application/json'
+      }
+      response = root.post('login/oauth/access_token', headers=headers, data=json.dumps(data))
+      result = cls._get_json(response)
+      return result['access_token']
+    except RestException, e:
+      raise GithubClientException('Failed to request access token from GitHub: %s' % e)
+    except KeyError:
+      raise GithubClientException('Failed to find access_token in GitHub oAuth response')
 
 
+  @classmethod
+  def is_authenticated(cls, access_token, **options):
+    api_url = options.get('api_url', GITHUB_API_URL.get()).strip('/')
+
+    try:
+      client = HttpClient(api_url, logger=LOG)
+      root = resource.Resource(client)
+      params = (
+        ('access_token', access_token),
+      )
+      root.get('user', params=params)
+      return True
+    except RestException:
+      return False
+
+
+  @classmethod
   def _get_json(cls, response):
     if type(response) != dict:
       try:
         response = json.loads(response)
       except ValueError:
-        raise GithubClientException('Github API did not return JSON response')
+        raise GithubClientException('GitHub API did not return JSON response')
     return response
 
 
+  @property
+  def github_url_regex(self):
+    return re.compile('%s/%s/%s/blob/%s/%s' % (self._github_base_url, self.OWNER_RE, self.REPO_RE, self.BRANCH_RE, self.FILEPATH_RE))
+
+
+  def _clean_path(self, filepath):
+    cleaned_path = filepath.strip('/')
+    cleaned_path = urllib.unquote(cleaned_path)
+    return cleaned_path
+
+
   def parse_github_url(self, url):
     """
     Given a base URL to a Github repository, return a tuple of the owner, repo, branch, and filepath
@@ -86,7 +149,7 @@ class GithubClient(object):
     if match:
       return match.group('owner'), match.group('repo'), match.group('branch'), match.group('filepath')
     else:
-      raise ValueError('Github URL is not formatted correctly: %s' % url)
+      raise ValueError('GitHub URL is not formatted correctly: %s' % url)
 
 
   def get_file_contents(self, owner, repo, filepath, branch='master'):
@@ -136,7 +199,7 @@ class GithubClient(object):
       response = self._root.get('repos/%s/%s/git/trees/%s' % (owner, repo, sha), headers=self.__headers, params=self.__params)
       return self._get_json(response)
     except RestException, e:
-      raise GithubClientException('Could not find Github object, check owner, repo and filepath or permissions: %s' % e)
+      raise GithubClientException('Could not find GitHub object, check owner, repo and filepath or permissions: %s' % e)
 
 
   def get_blob(self, owner, repo, sha):
@@ -148,4 +211,4 @@ class GithubClient(object):
       response = self._root.get('repos/%s/%s/git/blobs/%s' % (owner, repo, sha), headers=self.__headers, params=self.__params)
       return self._get_json(response)
     except RestException, e:
-      raise GithubClientException('Could not find Github object, check owner, repo and sha or permissions: %s' % e)
+      raise GithubClientException('Could not find GitHub object, check owner, repo and sha or permissions: %s' % e)

+ 2 - 0
desktop/libs/notebook/src/notebook/urls.py

@@ -63,6 +63,8 @@ urlpatterns += patterns('notebook.api',
 # Github
 urlpatterns += patterns('notebook.api',
   url(r'^api/github/fetch$', 'github_fetch', name='github_fetch'),
+  url(r'^api/github/authorize', 'github_authorize', name='github_authorize'),
+  url(r'^api/github/callback', 'github_callback', name='github_callback'),
 )
 
 # Assist API