Show More
@@ -153,18 +153,6 b' show_revision_number = false' | |||||
153 | ## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid> |
|
153 | ## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid> | |
154 | gist_alias_url = |
|
154 | gist_alias_url = | |
155 |
|
155 | |||
156 | ## white list of API enabled controllers. This allows to add list of |
|
|||
157 | ## controllers to which access will be enabled by api_key. eg: to enable |
|
|||
158 | ## api access to raw_files put `FilesController:raw`, to enable access to patches |
|
|||
159 | ## add `ChangesetController:changeset_patch`. This list should be "," separated |
|
|||
160 | ## Syntax is <ControllerClass>:<function>. Check debug logs for generated names |
|
|||
161 | ## Recommended settings below are commented out: |
|
|||
162 | api_access_controllers_whitelist = |
|
|||
163 | # ChangesetController:changeset_patch, |
|
|||
164 | # ChangesetController:changeset_raw, |
|
|||
165 | # FilesController:raw, |
|
|||
166 | # FilesController:archivefile |
|
|||
167 |
|
||||
168 | ## default encoding used to convert from and to unicode |
|
156 | ## default encoding used to convert from and to unicode | |
169 | ## can be also a comma separated list of encoding in case of mixed encodings |
|
157 | ## can be also a comma separated list of encoding in case of mixed encodings | |
170 | default_encoding = utf-8 |
|
158 | default_encoding = utf-8 |
@@ -1238,24 +1238,8 b' OUTPUT::' | |||||
1238 | API access for web views |
|
1238 | API access for web views | |
1239 | ------------------------ |
|
1239 | ------------------------ | |
1240 |
|
1240 | |||
1241 | API access can also be turned on for each web view in Kallithea that is |
|
1241 | Kallithea HTTP entry points can also be accessed without login using bearer | |
1242 | decorated with the ``@LoginRequired`` decorator. Some views use |
|
1242 | authentication by including this header with the request:: | |
1243 | ``@LoginRequired(api_access=True)`` and are always available. By default only |
|
|||
1244 | RSS/Atom feed views are enabled. Other views are |
|
|||
1245 | only available if they have been whitelisted. Edit the |
|
|||
1246 | ``api_access_controllers_whitelist`` option in your .ini file and define views |
|
|||
1247 | that should have API access enabled. |
|
|||
1248 |
|
||||
1249 | For example, to enable API access to patch/diff, raw file and archive:: |
|
|||
1250 |
|
||||
1251 | api_access_controllers_whitelist = |
|
|||
1252 | ChangesetController:changeset_patch, |
|
|||
1253 | ChangesetController:changeset_raw, |
|
|||
1254 | FilesController:raw, |
|
|||
1255 | FilesController:archivefile |
|
|||
1256 |
|
||||
1257 | After this change, a Kallithea view can be accessed without login using |
|
|||
1258 | bearer authentication, by including this header with the request:: |
|
|||
1259 |
|
1243 | |||
1260 | Authentication: Bearer <api_key> |
|
1244 | Authentication: Bearer <api_key> | |
1261 |
|
1245 |
@@ -51,7 +51,7 b' ttl = "5"' | |||||
51 |
|
51 | |||
52 | class FeedController(BaseRepoController): |
|
52 | class FeedController(BaseRepoController): | |
53 |
|
53 | |||
54 |
@LoginRequired( |
|
54 | @LoginRequired(allow_default_user=True) | |
55 | @HasRepoPermissionLevelDecorator('read') |
|
55 | @HasRepoPermissionLevelDecorator('read') | |
56 | def _before(self, *args, **kwargs): |
|
56 | def _before(self, *args, **kwargs): | |
57 | super(FeedController, self)._before(*args, **kwargs) |
|
57 | super(FeedController, self)._before(*args, **kwargs) |
@@ -220,7 +220,7 b' class JournalController(BaseController):' | |||||
220 |
|
220 | |||
221 | return render('journal/journal.html') |
|
221 | return render('journal/journal.html') | |
222 |
|
222 | |||
223 |
@LoginRequired( |
|
223 | @LoginRequired() | |
224 | def journal_atom(self): |
|
224 | def journal_atom(self): | |
225 | """ |
|
225 | """ | |
226 | Produce an atom-1.0 feed via feedgenerator module |
|
226 | Produce an atom-1.0 feed via feedgenerator module | |
@@ -231,7 +231,7 b' class JournalController(BaseController):' | |||||
231 | .all() |
|
231 | .all() | |
232 | return self._atom_feed(following, public=False) |
|
232 | return self._atom_feed(following, public=False) | |
233 |
|
233 | |||
234 |
@LoginRequired( |
|
234 | @LoginRequired() | |
235 | def journal_rss(self): |
|
235 | def journal_rss(self): | |
236 | """ |
|
236 | """ | |
237 | Produce an rss feed via feedgenerator module |
|
237 | Produce an rss feed via feedgenerator module | |
@@ -289,7 +289,7 b' class JournalController(BaseController):' | |||||
289 |
|
289 | |||
290 | return render('journal/public_journal.html') |
|
290 | return render('journal/public_journal.html') | |
291 |
|
291 | |||
292 |
@LoginRequired( |
|
292 | @LoginRequired(allow_default_user=True) | |
293 | def public_journal_atom(self): |
|
293 | def public_journal_atom(self): | |
294 | """ |
|
294 | """ | |
295 | Produce an atom-1.0 feed via feedgenerator module |
|
295 | Produce an atom-1.0 feed via feedgenerator module | |
@@ -301,7 +301,7 b' class JournalController(BaseController):' | |||||
301 |
|
301 | |||
302 | return self._atom_feed(c.following) |
|
302 | return self._atom_feed(c.following) | |
303 |
|
303 | |||
304 |
@LoginRequired( |
|
304 | @LoginRequired(allow_default_user=True) | |
305 | def public_journal_rss(self): |
|
305 | def public_journal_rss(self): | |
306 | """ |
|
306 | """ | |
307 | Produce an rss2 feed via feedgenerator module |
|
307 | Produce an rss2 feed via feedgenerator module |
@@ -367,28 +367,6 b' def _cached_perms_data(user_id, user_is_' | |||||
367 | return permissions |
|
367 | return permissions | |
368 |
|
368 | |||
369 |
|
369 | |||
370 | def allowed_api_access(controller_name, whitelist=None, api_key=None): |
|
|||
371 | """ |
|
|||
372 | Check if given controller_name is in whitelist API access |
|
|||
373 | """ |
|
|||
374 | if not whitelist: |
|
|||
375 | from kallithea import CONFIG |
|
|||
376 | whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'), |
|
|||
377 | sep=',') |
|
|||
378 | log.debug('whitelist of API access is: %s', whitelist) |
|
|||
379 | api_access_valid = controller_name in whitelist |
|
|||
380 | if api_access_valid: |
|
|||
381 | log.debug('controller:%s is in API whitelist', controller_name) |
|
|||
382 | else: |
|
|||
383 | msg = 'controller: %s is *NOT* in API whitelist' % (controller_name) |
|
|||
384 | if api_key: |
|
|||
385 | # if we use API key and don't have access it's a warning |
|
|||
386 | log.warning(msg) |
|
|||
387 | else: |
|
|||
388 | log.debug(msg) |
|
|||
389 | return api_access_valid |
|
|||
390 |
|
||||
391 |
|
||||
392 | class AuthUser(object): |
|
370 | class AuthUser(object): | |
393 | """ |
|
371 | """ | |
394 | Represents a Kallithea user, including various authentication and |
|
372 | Represents a Kallithea user, including various authentication and | |
@@ -689,13 +667,10 b' class LoginRequired(object):' | |||||
689 | If the "default" user is enabled and allow_default_user is true, that is |
|
667 | If the "default" user is enabled and allow_default_user is true, that is | |
690 | considered valid too. |
|
668 | considered valid too. | |
691 |
|
669 | |||
692 |
Also checks that IP address is allowed |
|
670 | Also checks that IP address is allowed. | |
693 | of regular cookie authentication, checks that API key access is allowed |
|
|||
694 | (based on `api_access` parameter and the API view whitelist). |
|
|||
695 | """ |
|
671 | """ | |
696 |
|
672 | |||
697 |
def __init__(self, |
|
673 | def __init__(self, allow_default_user=False): | |
698 | self.api_access = api_access |
|
|||
699 | self.allow_default_user = allow_default_user |
|
674 | self.allow_default_user = allow_default_user | |
700 |
|
675 | |||
701 | def __call__(self, func): |
|
676 | def __call__(self, func): | |
@@ -708,16 +683,9 b' class LoginRequired(object):' | |||||
708 | log.debug('Checking access for user %s @ %s', user, loc) |
|
683 | log.debug('Checking access for user %s @ %s', user, loc) | |
709 |
|
684 | |||
710 | # Check if we used an API key to authenticate. |
|
685 | # Check if we used an API key to authenticate. | |
711 |
|
|
686 | if user.authenticating_api_key is not None: | |
712 | if api_key is not None: |
|
|||
713 | # Check that controller is enabled for API key usage. |
|
|||
714 | if not self.api_access and not allowed_api_access(loc, api_key=api_key): |
|
|||
715 | # controller does not allow API access |
|
|||
716 | log.warning('API access to %s is not allowed', loc) |
|
|||
717 | raise HTTPForbidden() |
|
|||
718 |
|
||||
719 | log.info('user %s authenticated with API key ****%s @ %s', |
|
687 | log.info('user %s authenticated with API key ****%s @ %s', | |
720 | user, api_key[-4:], loc) |
|
688 | user, user.authenticating_api_key[-4:], loc) | |
721 | return func(*fargs, **fkwargs) |
|
689 | return func(*fargs, **fkwargs) | |
722 |
|
690 | |||
723 | # CSRF protection: Whenever a request has ambient authority (whether |
|
691 | # CSRF protection: Whenever a request has ambient authority (whether |
@@ -250,18 +250,6 b' show_revision_number = false' | |||||
250 | <%text>## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid></%text> |
|
250 | <%text>## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid></%text> | |
251 | gist_alias_url = |
|
251 | gist_alias_url = | |
252 |
|
252 | |||
253 | <%text>## white list of API enabled controllers. This allows to add list of</%text> |
|
|||
254 | <%text>## controllers to which access will be enabled by api_key. eg: to enable</%text> |
|
|||
255 | <%text>## api access to raw_files put `FilesController:raw`, to enable access to patches</%text> |
|
|||
256 | <%text>## add `ChangesetController:changeset_patch`. This list should be "," separated</%text> |
|
|||
257 | <%text>## Syntax is <ControllerClass>:<function>. Check debug logs for generated names</%text> |
|
|||
258 | <%text>## Recommended settings below are commented out:</%text> |
|
|||
259 | api_access_controllers_whitelist = |
|
|||
260 | # ChangesetController:changeset_patch, |
|
|||
261 | # ChangesetController:changeset_raw, |
|
|||
262 | # FilesController:raw, |
|
|||
263 | # FilesController:archivefile |
|
|||
264 |
|
||||
265 | <%text>## default encoding used to convert from and to unicode</%text> |
|
253 | <%text>## default encoding used to convert from and to unicode</%text> | |
266 | <%text>## can be also a comma separated list of encoding in case of mixed encodings</%text> |
|
254 | <%text>## can be also a comma separated list of encoding in case of mixed encodings</%text> | |
267 | default_encoding = utf-8 |
|
255 | default_encoding = utf-8 |
@@ -441,10 +441,6 b' class TestLoginController(TestController' | |||||
441 | # API |
|
441 | # API | |
442 | #========================================================================== |
|
442 | #========================================================================== | |
443 |
|
443 | |||
444 | def _get_api_whitelist(self, values=None): |
|
|||
445 | config = {'api_access_controllers_whitelist': values or []} |
|
|||
446 | return config |
|
|||
447 |
|
||||
448 | def _api_key_test(self, api_key, status): |
|
444 | def _api_key_test(self, api_key, status): | |
449 | """Verifies HTTP status code for accessing an auth-requiring page, |
|
445 | """Verifies HTTP status code for accessing an auth-requiring page, | |
450 | using the given api_key URL parameter as well as using the API key |
|
446 | using the given api_key URL parameter as well as using the API key | |
@@ -476,45 +472,22 b' class TestLoginController(TestController' | |||||
476 | ('none', None, 302), |
|
472 | ('none', None, 302), | |
477 | ('empty_string', '', 403), |
|
473 | ('empty_string', '', 403), | |
478 | ('fake_number', '123456', 403), |
|
474 | ('fake_number', '123456', 403), | |
479 | ('proper_api_key', True, 403) |
|
|||
480 | ]) |
|
|||
481 | def test_access_not_whitelisted_page_via_api_key(self, test_name, api_key, code): |
|
|||
482 | whitelist = self._get_api_whitelist([]) |
|
|||
483 | with mock.patch('kallithea.CONFIG', whitelist): |
|
|||
484 | assert [] == whitelist['api_access_controllers_whitelist'] |
|
|||
485 | self._api_key_test(api_key, code) |
|
|||
486 |
|
||||
487 | @parametrize('test_name,api_key,code', [ |
|
|||
488 | ('none', None, 302), |
|
|||
489 | ('empty_string', '', 403), |
|
|||
490 | ('fake_number', '123456', 403), |
|
|||
491 | ('fake_not_alnum', 'a-z', 403), |
|
475 | ('fake_not_alnum', 'a-z', 403), | |
492 | ('fake_api_key', '0123456789abcdef0123456789ABCDEF01234567', 403), |
|
476 | ('fake_api_key', '0123456789abcdef0123456789ABCDEF01234567', 403), | |
493 | ('proper_api_key', True, 200) |
|
477 | ('proper_api_key', True, 200) | |
494 | ]) |
|
478 | ]) | |
495 |
def test_access_ |
|
479 | def test_access_page_via_api_key(self, test_name, api_key, code): | |
496 | whitelist = self._get_api_whitelist(['ChangesetController:changeset_raw']) |
|
480 | self._api_key_test(api_key, code) | |
497 | with mock.patch('kallithea.CONFIG', whitelist): |
|
|||
498 | assert ['ChangesetController:changeset_raw'] == whitelist['api_access_controllers_whitelist'] |
|
|||
499 | self._api_key_test(api_key, code) |
|
|||
500 |
|
481 | |||
501 | def test_access_page_via_extra_api_key(self): |
|
482 | def test_access_page_via_extra_api_key(self): | |
502 | whitelist = self._get_api_whitelist(['ChangesetController:changeset_raw']) |
|
483 | new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test') | |
503 | with mock.patch('kallithea.CONFIG', whitelist): |
|
484 | Session().commit() | |
504 | assert ['ChangesetController:changeset_raw'] == whitelist['api_access_controllers_whitelist'] |
|
485 | self._api_key_test(new_api_key.api_key, status=200) | |
505 |
|
||||
506 | new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test') |
|
|||
507 | Session().commit() |
|
|||
508 | self._api_key_test(new_api_key.api_key, status=200) |
|
|||
509 |
|
486 | |||
510 | def test_access_page_via_expired_api_key(self): |
|
487 | def test_access_page_via_expired_api_key(self): | |
511 | whitelist = self._get_api_whitelist(['ChangesetController:changeset_raw']) |
|
488 | new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test') | |
512 | with mock.patch('kallithea.CONFIG', whitelist): |
|
489 | Session().commit() | |
513 | assert ['ChangesetController:changeset_raw'] == whitelist['api_access_controllers_whitelist'] |
|
490 | # patch the API key and make it expired | |
514 |
|
491 | new_api_key.expires = 0 | ||
515 | new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test') |
|
492 | Session().commit() | |
516 | Session().commit() |
|
493 | self._api_key_test(new_api_key.api_key, status=403) | |
517 | # patch the API key and make it expired |
|
|||
518 | new_api_key.expires = 0 |
|
|||
519 | Session().commit() |
|
|||
520 | self._api_key_test(new_api_key.api_key, status=403) |
|
General Comments 0
You need to be logged in to leave comments.
Login now