diff --git a/docs/api/api.rst b/docs/api/api.rst --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -1019,8 +1019,14 @@ For example, to enable API access to pat FilesController:raw, FilesController:archivefile -After this change, a Kallithea view can be accessed without login by adding a -GET parameter ``?api_key=`` to the URL. +After this change, a Kallithea view can be accessed without login using +bearer authentication, by including this header with the request:: + + Authentication: Bearer + +Alternatively, the API key can be passed in the URL query string using +``?api_key=``, though this is not recommended due to the increased +risk of API key leaks, and support will likely be removed in the future. Exposing raw diffs is a good way to integrate with third-party services like code review, or build farms that can download archives. diff --git a/kallithea/lib/base.py b/kallithea/lib/base.py --- a/kallithea/lib/base.py +++ b/kallithea/lib/base.py @@ -365,11 +365,15 @@ class BaseController(WSGIController): self.scm_model = ScmModel(self.sa) @staticmethod - def _determine_auth_user(api_key, session_authuser): + def _determine_auth_user(api_key, bearer_token, session_authuser): + """ + Create an `AuthUser` object given the API key/bearer token + (if any) and the value of the authuser session cookie. """ - Create an `AuthUser` object given the API key (if any) and the - value of the authuser session cookie. - """ + + # Authenticate by bearer token + if bearer_token is not None: + api_key = bearer_token # Authenticate by API key if api_key is not None: @@ -459,8 +463,20 @@ class BaseController(WSGIController): self._basic_security_checks() #set globals for auth user + + bearer_token = None + try: + # Request.authorization may raise ValueError on invalid input + type, params = request.authorization + except (ValueError, TypeError): + pass + else: + if type.lower() == 'bearer': + bearer_token = params + self.authuser = c.authuser = request.user = self._determine_auth_user( request.GET.get('api_key'), + bearer_token, session.get('authuser'), ) diff --git a/kallithea/lib/utils2.py b/kallithea/lib/utils2.py --- a/kallithea/lib/utils2.py +++ b/kallithea/lib/utils2.py @@ -131,7 +131,14 @@ def detect_mode(line, default): def generate_api_key(): """ Generates a random (presumably unique) API key. + + This value is used in URLs and "Bearer" HTTP Authorization headers, + which in practice means it should only contain URL-safe characters + (RFC 3986): + + unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" """ + # Hexadecimal certainly qualifies as URL-safe. return binascii.hexlify(os.urandom(20)) diff --git a/kallithea/tests/functional/test_login.py b/kallithea/tests/functional/test_login.py --- a/kallithea/tests/functional/test_login.py +++ b/kallithea/tests/functional/test_login.py @@ -435,22 +435,31 @@ class TestLoginController(TestController def _api_key_test(self, api_key, status): """Verifies HTTP status code for accessing an auth-requiring page, - using the given api_key URL parameter. If api_key is None, no api_key - parameter is passed at all. If api_key is True, a real, working API key - is used. + using the given api_key URL parameter as well as using the API key + with bearer authentication. + + If api_key is None, no api_key is passed at all. If api_key is True, + a real, working API key is used. """ with fixture.anon_access(False): if api_key is None: params = {} + headers = {} else: if api_key is True: api_key = User.get_first_admin().api_key params = {'api_key': api_key} + headers = {'Authorization': 'Bearer ' + str(api_key)} self.app.get(url(controller='changeset', action='changeset_raw', repo_name=HG_REPO, revision='tip', **params), status=status) + self.app.get(url(controller='changeset', action='changeset_raw', + repo_name=HG_REPO, revision='tip'), + headers=headers, + status=status) + @parametrize('test_name,api_key,code', [ ('none', None, 302), ('empty_string', '', 403),