Repository: Kinto/kinto

Issue:
Accounts post API crashes with unset id (if basicauth is enabled too)
```
gsurita-30820:~ gsurita$ echo '{"data": {"password": "me"}}' | http post localhost:8888/v1/accounts -a foo:bar
HTTP/1.1 500 Internal Server Error
(...)
```

```
Traceback (most recent call last):
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/tweens.py", line 22, in excview_tween
    response = handler(request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid_tm/__init__.py", line 119, in tm_tween
    reraise(*exc_info)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid_tm/compat.py", line 15, in reraise
    raise value
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid_tm/__init__.py", line 98, in tm_tween
    response = handler(request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/router.py", line 155, in handle_request
    view_name
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/view.py", line 612, in _call_view
    response = view_callable(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/config/views.py", line 181, in __call__
    return view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 389, in attr_view
    return view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 367, in predicate_wrapper
    return view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 300, in secured_view
    return view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 438, in rendered_view
    result = view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 147, in _requestonly_view
    response = view(request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/cornice/service.py", line 491, in wrapper
    response = view_()
  File "/Users/gsurita/kinto/kinto/kinto/plugins/accounts/views.py", line 81, in collection_post
    result = super(Account, self).collection_post()
  File "/Users/gsurita/kinto/kinto/kinto/core/resource/__init__.py", line 341, in collection_post
    new_record = self.process_record(new_record)
  File "/Users/gsurita/kinto/kinto/kinto/plugins/accounts/views.py", line 102, in process_record
    if new[self.model.id_field] != self.request.selected_userid:
KeyError: 'id'
```
Accounts post API crashes with unset id (if basicauth is enabled too)
```
gsurita-30820:~ gsurita$ echo '{"data": {"password": "me"}}' | http post localhost:8888/v1/accounts -a foo:bar
HTTP/1.1 500 Internal Server Error
(...)
```

```
Traceback (most recent call last):
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/tweens.py", line 22, in excview_tween
    response = handler(request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid_tm/__init__.py", line 119, in tm_tween
    reraise(*exc_info)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid_tm/compat.py", line 15, in reraise
    raise value
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid_tm/__init__.py", line 98, in tm_tween
    response = handler(request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/router.py", line 155, in handle_request
    view_name
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/view.py", line 612, in _call_view
    response = view_callable(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/config/views.py", line 181, in __call__
    return view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 389, in attr_view
    return view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 367, in predicate_wrapper
    return view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 300, in secured_view
    return view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 438, in rendered_view
    result = view(context, request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/pyramid/viewderivers.py", line 147, in _requestonly_view
    response = view(request)
  File "/Users/gsurita/kinto/kinto/.venv/lib/python3.6/site-packages/cornice/service.py", line 491, in wrapper
    response = view_()
  File "/Users/gsurita/kinto/kinto/kinto/plugins/accounts/views.py", line 81, in collection_post
    result = super(Account, self).collection_post()
  File "/Users/gsurita/kinto/kinto/kinto/core/resource/__init__.py", line 341, in collection_post
    new_record = self.process_record(new_record)
  File "/Users/gsurita/kinto/kinto/kinto/plugins/accounts/views.py", line 102, in process_record
    if new[self.model.id_field] != self.request.selected_userid:
KeyError: 'id'
```


Discussion:
Just realized basicauth was also enabled. We should maybe clarify in the documentation that's not possible to use both at the same time.

In theory we should be able to use both (I mean for consistency). I suggest that we keep this open and investigate what we can do (fix bug, document limitation, raise configurationerror ... )
> In theory we should be able to use both (I mean for consistency).

How would that work? Check if an account exist and use it otherwise generate a basicauth principal? I don't know, this sounds a bit confusing and I can't think of an use case for that besides integration tests on clients. Maybe raising `ConfigurationError` is an option.

But now that we are here, it's probably a good idea to test (and support) integration with portier and other authentication plugins.
> How would that work? Check if an account exist and use it otherwise generate a basicauth principal?

We rely on Pyramid Multiauth. It iterates on every auth methods until one is successful.

I agree this can be very confusing. For example in prod, we once had ldap+basicauth and when users entered wrong ldap credentials they would end up authenticated as `basicauth:*` instead of receiving 401.
Just realized basicauth was also enabled. We should maybe clarify in the documentation that's not possible to use both at the same time.

In theory we should be able to use both (I mean for consistency). I suggest that we keep this open and investigate what we can do (fix bug, document limitation, raise configurationerror ... )
> In theory we should be able to use both (I mean for consistency).

How would that work? Check if an account exist and use it otherwise generate a basicauth principal? I don't know, this sounds a bit confusing and I can't think of an use case for that besides integration tests on clients. Maybe raising `ConfigurationError` is an option.

But now that we are here, it's probably a good idea to test (and support) integration with portier and other authentication plugins.
> How would that work? Check if an account exist and use it otherwise generate a basicauth principal?

We rely on Pyramid Multiauth. It iterates on every auth methods until one is successful.

I agree this can be very confusing. For example in prod, we once had ldap+basicauth and when users entered wrong ldap credentials they would end up authenticated as `basicauth:*` instead of receiving 401.

Resolving patch:
diff --git a/kinto/plugins/accounts/__init__.py b/kinto/plugins/accounts/__init__.py
--- a/kinto/plugins/accounts/__init__.py
+++ b/kinto/plugins/accounts/__init__.py
@@ -1,4 +1,5 @@
 from kinto.authorization import PERMISSIONS_INHERITANCE_TREE
+from pyramid.exceptions import ConfigurationError
 
 
 def includeme(config):
@@ -16,3 +17,12 @@ def includeme(config):
         'write': {'account': ['write']},
         'read': {'account': ['write', 'read']}
     }
+
+    # Add some safety to avoid weird behaviour with basicauth default policy.
+    settings = config.get_settings()
+    auth_policies = settings['multiauth.policies']
+    if 'basicauth' in auth_policies and 'account' in auth_policies:
+        if auth_policies.index('basicauth') < auth_policies.index('account'):
+            error_msg = ("'basicauth' should not be mentioned before 'account' "
+                         "in 'multiauth.policies' setting.")
+            raise ConfigurationError(error_msg)


Test patch:
diff --git a/tests/plugins/test_accounts.py b/tests/plugins/test_accounts.py
--- a/tests/plugins/test_accounts.py
+++ b/tests/plugins/test_accounts.py
@@ -4,6 +4,7 @@
 import unittest
 
 from kinto.core.testing import get_user_headers
+from pyramid.exceptions import ConfigurationError
 
 from .. import support
 
@@ -12,14 +13,15 @@ class AccountsWebTest(support.BaseWebTest, unittest.TestCase):
 
     @classmethod
     def get_app_settings(cls, extras=None):
-        settings = super().get_app_settings(extras)
-        settings['includes'] = 'kinto.plugins.accounts'
-        settings['account_create_principals'] = 'system.Everyone'
-        settings['multiauth.policies'] = 'account'
+        if extras is None:
+            extras = {}
+        extras.setdefault('multiauth.policies', 'account')
+        extras.setdefault('includes', 'kinto.plugins.accounts')
+        extras.setdefault('account_create_principals', 'system.Everyone')
         # XXX: this should be a default setting.
-        settings['multiauth.policy.account.use'] = ('kinto.plugins.accounts.authentication.'
-                                                    'AccountsAuthenticationPolicy')
-        return settings
+        extras.setdefault('multiauth.policy.account.use', 'kinto.plugins.accounts.authentication.'
+                                                          'AccountsAuthenticationPolicy')
+        return super().get_app_settings(extras)
 
 
 class HelloViewTest(AccountsWebTest):
@@ -242,3 +244,31 @@ def test_user_created_by_admin_can_delete_her_record(self):
                            headers=self.admin_headers)
 
         self.app.delete('/accounts/alice', headers=get_user_headers('alice', 'bouh'))
+
+
+class WithBasicAuthTest(AccountsWebTest):
+
+    @classmethod
+    def get_app_settings(cls, extras=None):
+        if extras is None:
+            extras = {'multiauth.policies': 'account basicauth'}
+        return super().get_app_settings(extras)
+
+    def test_password_field_is_mandatory(self):
+        self.app.post_json('/accounts', {'data': {'id': 'me'}}, status=400)
+
+    def test_id_field_is_mandatory(self):
+        self.app.post_json('/accounts', {'data': {'password': 'pass'}}, status=400)
+
+    def test_fallsback_on_basicauth(self):
+        self.app.post_json('/accounts', {'data': {'id': 'me', 'password': 'bleh'}})
+
+        resp = self.app.get('/', headers=get_user_headers('me', 'wrong'))
+        assert 'basicauth' in resp.json['user']['id']
+
+        resp = self.app.get('/', headers=get_user_headers('me', 'bleh'))
+        assert 'account' in resp.json['user']['id']
+
+    def test_raise_configuration_if_wrong_error(self):
+        with self.assertRaises(ConfigurationError):
+            self.make_app({'multiauth.policies': 'basicauth account'})