Repository: ckan/ckan

Issue:
Cannot create API token via API (hen and egg)
## CKAN version 2.10.1

## Describe the bug
CKAN 2.9 supported API keys which made it possible to create a user via the API (`ckan.auth.create_user_via_api = true`) and automatically return the user's API key (`ckan.auth.create_default_api_keys = true`).

Since CKAN 2.10 does not support API keys, it is not possible anymore to use the API anonymously to create users and start working as them. An API token is required to create an API token for a user. So users would have to log in via the web interface first to create an API token.



Discussion:
Let's add an optional parameter to `user_create` or a new endpoint that creates a user and an API token at the same time
> So users would have to log in via the web interface first to create an API token.

Just to be sure, if you use a sysadmin's token you can call [`api_token_create`](https://docs.ckan.org/en/2.10/api/index.html#ckan.logic.action.create.api_token_create) on the newly created user to get a token for that user back.

I don't know, but adding the option to create API tokens anonymously to users that already can be created anonymously (if `ckan.auth.create_user_via_api = true`) seems to make things too easy for spammers, even if this is turned off by default.


> Let's add an optional parameter to user_create or a new endpoint that creates a user and an API token at the same time

For authenticated users (sysadmins) I think the best option is a new `create_api_token` param. Note that API tokens creation requires at least a name, and may require additional parameters depending on plugins installed (eg `expire_api_token`) so we need to include a parameter for those as well


> > So users would have to log in via the web interface first to create an API token.
> 
> Just to be sure, if you use a sysadmin's token you can call [`api_token_create`](https://docs.ckan.org/en/2.10/api/index.html#ckan.logic.action.create.api_token_create) on the newly created user to get a token for that user back.
> 
> I don't know, but adding the option to create API tokens anonymously to users that already can be created anonymously (if `ckan.auth.create_user_via_api = true`) seems to make things too easy for spammers, even if this is turned off by default.

My use case is this: I have a testing CKAN instance where users can connect to with a software that I have written to familiarize themselves with it. I wanted to make signup of test accounts as simple as possible (click of a button in the application). I cannot use an admin account (shipping an admin API token with the application is not an option). The user has to create the token after an account is created with the API. I could also automate it with `curl` scripts, but using the API would be the preferred option.


Resolving patch:
diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py
--- a/ckan/logic/action/create.py
+++ b/ckan/logic/action/create.py
@@ -965,7 +965,9 @@ def user_create(context: Context,
                 }
             }
     :type plugin_extras: dict
-
+    :param with_apitoken: whether to create an API token for the user.
+                    (Optional)
+    :type with_apitoken: bool
 
     :returns: the newly created user
     :rtype: dictionary
@@ -974,6 +976,7 @@ def user_create(context: Context,
     model = context['model']
     schema = context.get('schema') or ckan.logic.schema.default_user_schema()
     session = context['session']
+    with_apitoken = data_dict.pop("with_apitoken", False)
 
     _check_access('user_create', context, data_dict)
 
@@ -1032,10 +1035,21 @@ def user_create(context: Context,
 
     # Create dashboard for user.
     dashboard = model.Dashboard(user.id)
+
     session.add(dashboard)
     if not context.get('defer_commit'):
         model.repo.commit()
 
+    if with_apitoken:
+        if not context['user']:
+            context["user"] = user.name
+
+        # Create apitoken for user.
+        api_token = _get_action("api_token_create")(
+            context, {"user": user.name, "name": "default"}
+        )
+        user_dict["token"] = api_token["token"]
+
     log.debug('Created user {name}'.format(name=user.name))
     return user_dict
 


Test patch:
diff --git a/ckan/tests/logic/action/test_create.py b/ckan/tests/logic/action/test_create.py
--- a/ckan/tests/logic/action/test_create.py
+++ b/ckan/tests/logic/action/test_create.py
@@ -1270,6 +1270,41 @@ def test_user_create_defer_commit(self):
         with pytest.raises(logic.NotFound):
             helpers.call_action("user_show", id=user_dict["name"])
 
+    def test_create_user_with_apitoken(self):
+        stub = factories.User.stub()
+        context = {"ignore_auth": True}
+        user_dict = {
+            "name": stub.name,
+            "email": stub.email,
+            "password": "test1234",
+            "with_apitoken": True
+        }
+        user = helpers.call_action("user_create", context={}, **user_dict)
+        assert user["token"]
+
+        user_dict = {"user_id": user["name"]}
+        token = helpers.call_action(
+            "api_token_list", context=context, **user_dict
+        )
+        assert len(token) == 1
+
+    def test_create_user_with_apitoken_missing_flag(self):
+        stub = factories.User.stub()
+        context = {"ignore_auth": True}
+        user_dict = {
+            "name": stub.name,
+            "email": stub.email,
+            "password": "test1234",
+        }
+        user = helpers.call_action("user_create", context={}, **user_dict)
+        assert "token" not in user
+
+        user_dict = {"user_id": user["name"]}
+        token = helpers.call_action(
+            "api_token_list", context=context, **user_dict
+        )
+        assert not token
+
 
 @pytest.mark.usefixtures("clean_db")
 @pytest.mark.ckan_config("ckan.auth.create_user_via_web", True)