Repository: Kinto/kinto

Issue:
Accounts parent_id in records table not being set correctly
Ubuntu 16.04
From source Latest commit 5e2a13b
Postgres database

1. Change kinto.ini to enable accounts
2. Create admin user myadmin with
`kinto.account_create_principals = system.Everyone
`
3. Then change to
`kinto.account_create_principals = account:myadmin`

Final kinto.ini changes
```
multiauth.policies = account
multiauth.policy.account.use = kinto.plugins.accounts.authentication.AccountsAuthenticationPolicy
# Allow anyone to create accounts.
kinto.account_create_principals = account:myadmin
# Set user 'account:admin' as the administrator.
kinto.account_write_principals = account:myadmin
kinto.account_read_principals = account:myadmin

```

4.  Create user using myadmin auth
echo '{"data": {"id": "zzz", "password": "test"}}' | http POST http://192.168.2.12:8888/v1/accounts --auth 'myadmin:test'

5. Try to delete user
`http DELETE http://192.168.2.12:8888/v1/accounts/zzz --verbose --auth 'myadmin:test'`

```
{
    "code": 404,
    "details": {
        "id": "zzz",
        "resource_name": "account"
    },
    "errno": 110,
    "error": "Not Found"
}
```

6. If you change user zzz's password then Delete is a success
echo '{"data": {"password": "asdf"}}' | http PUT http://localhost:8888/v1/accounts/zzz --verbose --auth 'myadmin:test'

If you look at the records table in Postgres you will see that the initial creation of user zzz's parent_id is set to *

After changing zzz's password parent_id is set to zzz

Accounts parent_id in records table not being set correctly
Ubuntu 16.04
From source Latest commit 5e2a13b
Postgres database

1. Change kinto.ini to enable accounts
2. Create admin user myadmin with
`kinto.account_create_principals = system.Everyone
`
3. Then change to
`kinto.account_create_principals = account:myadmin`

Final kinto.ini changes
```
multiauth.policies = account
multiauth.policy.account.use = kinto.plugins.accounts.authentication.AccountsAuthenticationPolicy
# Allow anyone to create accounts.
kinto.account_create_principals = account:myadmin
# Set user 'account:admin' as the administrator.
kinto.account_write_principals = account:myadmin
kinto.account_read_principals = account:myadmin

```

4.  Create user using myadmin auth
echo '{"data": {"id": "zzz", "password": "test"}}' | http POST http://192.168.2.12:8888/v1/accounts --auth 'myadmin:test'

5. Try to delete user
`http DELETE http://192.168.2.12:8888/v1/accounts/zzz --verbose --auth 'myadmin:test'`

```
{
    "code": 404,
    "details": {
        "id": "zzz",
        "resource_name": "account"
    },
    "errno": 110,
    "error": "Not Found"
}
```

6. If you change user zzz's password then Delete is a success
echo '{"data": {"password": "asdf"}}' | http PUT http://localhost:8888/v1/accounts/zzz --verbose --auth 'myadmin:test'

If you look at the records table in Postgres you will see that the initial creation of user zzz's parent_id is set to *

After changing zzz's password parent_id is set to zzz



Discussion:
Thank you very much for this detailed bug report! 

The account resource contains some hacks to be able to allow anonymous creation and still have a `parent_id` that makes its content private to each user. We'll look into it :)
Thank you very much for this detailed bug report! 

The account resource contains some hacks to be able to allow anonymous creation and still have a `parent_id` that makes its content private to each user. We'll look into it :)

Resolving patch:
diff --git a/kinto/plugins/accounts/views.py b/kinto/plugins/accounts/views.py
--- a/kinto/plugins/accounts/views.py
+++ b/kinto/plugins/accounts/views.py
@@ -10,6 +10,23 @@
 from kinto.core.errors import raise_invalid, http_error
 
 
+def _extract_posted_body_id(request):
+    try:
+        # Anonymous creation with POST.
+        return request.json['data']['id']
+    except (ValueError, KeyError):
+        # Bad POST data.
+        if request.method.lower() == 'post':
+            error_details = {
+                'name': 'data.id',
+                'description': 'data.id in body: Required'
+            }
+            raise_invalid(request, **error_details)
+        # Anonymous GET
+        error_msg = 'Cannot read accounts.'
+        raise http_error(httpexceptions.HTTPUnauthorized(), error=error_msg)
+
+
 class AccountSchema(resource.ResourceSchema):
     password = colander.SchemaNode(colander.String())
 
@@ -30,7 +47,7 @@ def __init__(self, request, context):
         super().__init__(request, context)
 
         # Overwrite the current principal set by ShareableResource.
-        if self.model.current_principal == Everyone:
+        if self.model.current_principal == Everyone or context.is_administrator:
             # Creation is anonymous, but author with write perm is this:
             # XXX: only works if policy name is account in settings.
             self.model.current_principal = 'account:{}'.format(self.model.parent_id)
@@ -48,8 +65,12 @@ def get_parent_id(self, request):
         # to the same value they would obtain when authenticated.
         if self.context.is_administrator:
             if self.context.on_collection:
-                # Admin see all accounts.
-                return '*'
+                # Accounts created by admin should have userid as parent.
+                if request.method.lower() == 'post':
+                    return _extract_posted_body_id(request)
+                else:
+                    # Admin see all accounts.
+                    return '*'
             else:
                 # No pattern matching for admin on single record.
                 return request.matchdict['id']
@@ -62,20 +83,7 @@ def get_parent_id(self, request):
         if 'id' in request.matchdict:
             return request.matchdict['id']
 
-        try:
-            # Anonymous creation with POST.
-            return request.json['data']['id']
-        except (ValueError, KeyError):
-            # Bad POST data.
-            if request.method.lower() == 'post':
-                error_details = {
-                    'name': 'data.id',
-                    'description': 'data.id in body: Required'
-                }
-                raise_invalid(request, **error_details)
-            # Anonymous GET
-            error_msg = 'Cannot read accounts.'
-            raise http_error(httpexceptions.HTTPUnauthorized(), error=error_msg)
+        return _extract_posted_body_id(request)
 
     def collection_post(self):
         result = super(Account, self).collection_post()


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
@@ -219,3 +219,26 @@ def test_admin_can_retrieve_accounts_list(self):
         resp = self.app.get('/accounts', headers=self.admin_headers)
         usernames = [r['id'] for r in resp.json['data']]
         assert sorted(usernames) == ['admin', 'alice', 'bob']
+
+    def test_user_created_by_admin_with_put_can_see_her_record(self):
+        self.app.put_json('/accounts/alice',
+                          {'data': {'password': 'bouh'}},
+                          headers=self.admin_headers)
+
+        resp = self.app.get('/accounts/alice', headers=get_user_headers('alice', 'bouh'))
+        assert resp.json['permissions'] == {'write': ['account:alice']}
+
+    def test_user_created_by_admin_with_post_can_see_her_record(self):
+        self.app.post_json('/accounts',
+                           {'data': {'id': 'alice', 'password': 'bouh'}},
+                           headers=self.admin_headers)
+
+        resp = self.app.get('/accounts/alice', headers=get_user_headers('alice', 'bouh'))
+        assert resp.json['permissions'] == {'write': ['account:alice']}
+
+    def test_user_created_by_admin_can_delete_her_record(self):
+        self.app.post_json('/accounts',
+                           {'data': {'id': 'alice', 'password': 'bouh'}},
+                           headers=self.admin_headers)
+
+        self.app.delete('/accounts/alice', headers=get_user_headers('alice', 'bouh'))