Repository: ckan/ckan

Issue:
user_invite crashes if generated password does not validate
The `user_invite` function generates a hidden password that will pass the built-in validators, but if custom validators are supplied and the generated password fails, the function will crash with a KeyError ('password' does not exist in the error dict).

Furthermore, code review suggests that if the crash were fixed, it would be in danger of an infinite loop, constantly generating passwords that are not accepted.

### CKAN Version if known (or site URL)

CKAN 2.8.3

### Please describe the expected behaviour

Inviting a new member, identified by email address, to join an organisation, should result in an email to that address.

### Please describe the actual behaviour

Internal server error. Stack trace shows that this is a `KeyError: ('password',)` in `user_password_validator`.

### What steps can be taken to reproduce the issue?

- Add a password validator that requires non-alphanumeric symbols.
- Go to an organisation, select Manage-Members.
- Select an email address and Add Member.



Discussion:
It seems to me that there are several fixes that should be applied:

- `user_password_validator` should initialize the `password` error key if it does not exist.
- Alternatively, `user_invite` should populate the `errors` dict with a blank `password` key before passing it to the validator function.
- The `test_validators` class should be updated with tests that pass in empty `errors` dictionaries.
- `user_invite` should limit the number of times it will retry password generation if validation fails.
- `user_invite` should use a stronger password generation mechanism; the current implementation never uses non-alphanumeric symbols, so it will always fail some types of custom validation.
Note that CKAN 2.7 handled the error somewhat more gracefully, displaying the validation failure on-screen, but still had no way to fix the problem if the validators were too strict for the password generation code.
Two proposals from our developer call on resolving this one:

1. Change the way invites work so that they send a token that allows the user to create a new account with specific permissions (don't create the user with a fake password and reuse the password reset tokens)

2. Add a "password_disabled" option to `user_create` and `user_update` to allow creating a user with no password set (stored as an invalid hash that no password would match) This could also be used for other accounts that don't need a password, such as the site user and users authenticated through other services.
Those would be good fixes too.
@ThrawnCA marked as good for contribution, if you have time to work on either approach I'm happy to review and merge. :-)
@wardi @ThrawnCA 
We would like to work on this issue. Please me know if no one is working on this.
@Chandradeep-NEC No, no progress here. Go ahead.
@ThrawnCA, I am trying to reproduce the issue. I am facing issue related to SMTP server. I have configure SMTP server on my local machine. When I tried to add a member via email, Internal server error appear on GUI and invitation is not received on the entered email address, but user is created on GUI. To confirm the same I have configure external SMTP server and tried to add a member, Internal sever error did not appear on GUI  this time and invitation is received on the entered email address and user is created. Which SMTP server approach you have taken while producing the issue?
Are you using a custom password validator? If you use the standard built-in validator, then it will work.
@ThrawnCA Please confirm as have you written a new function for user_password_validator to get custom password validator?
@Gauravp-NEC Yes; see https://github.com/qld-gov-au/ckan-ex-qgov/blob/master/ckanext/qgov/common/intercepts.py (probably not the most elegant way to implement it, but the same problem would arise with any other implementation that imposed stricter complexity requirements).
@wardi , as you suggested the way to solve this issue in comment https://github.com/ckan/ckan/issues/4986#issuecomment-533128823 no work have been done till now by anyone and this issue is open for so long. 

I suggest that we should change the logic of def user_invite() in such a way that generated random password should contain at least 1 Upper case, 1 Lower case, 1 Special character and 1 Digit. so that it will never fail for any standard custom validation. Please let me know your opinion. If you agree I will raise the PR for it.
@Gauravp-NEC that would fix the problem only until someone comes up with a new password validation that doesn't work with these generated passwords. So it doesn't really fix the problem.

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
@@ -34,7 +34,7 @@
 import ckan.model
 
 from ckan.common import _
-from ckan.types import Context, DataDict, ErrorDict, Schema, FlattenErrorDict
+from ckan.types import Context, DataDict, ErrorDict, Schema
 
 # FIXME this looks nasty and should be shared better
 from ckan.logic.action.update import _update_package_relationship
@@ -1046,7 +1046,6 @@ def user_invite(context: Context,
     :returns: the newly created user
     :rtype: dictionary
     '''
-    import string
     _check_access('user_invite', context, data_dict)
 
     schema = context.get('schema',
@@ -1061,24 +1060,16 @@ def user_invite(context: Context,
         raise NotFound()
 
     name = _get_random_username_from_email(data['email'])
-    # Choose a password. However it will not be used - the invitee will not be
-    # told it - they will need to reset it
-    while True:
-        password = ''.join(random.SystemRandom().choice(
-            string.ascii_lowercase + string.ascii_uppercase + string.digits)
-            for _ in range(12))
-        # Occasionally it won't meet the constraints, so check
-        validation_errors: FlattenErrorDict = {}
-        ckan.logic.validators.user_password_validator(
-            ('password', ), {('password', ): password},
-            validation_errors, context)
-        if not validation_errors:
-            break
 
     data['name'] = name
-    data['password'] = password
+    # send the proper schema when creating a user from here
+    # so the password field would be ignored.
+    invite_schema = ckan.logic.schema.create_user_for_user_invite_schema()
+
     data['state'] = model.State.PENDING
-    user_dict = _get_action('user_create')(context, data)
+    user_dict = _get_action('user_create')(
+        cast(Context, dict(context, schema=invite_schema)),
+        data)
     user = model.User.get(user_dict['id'])
     assert user
     member_dict = {
@@ -1087,14 +1078,11 @@ def user_invite(context: Context,
         'role': data['role']
     }
 
-    if group.is_organization:
-        _get_action('organization_member_create')(context, member_dict)
-        group_dict = _get_action('organization_show')(context,
-                                                      {'id': data['group_id']})
-    else:
-        _get_action('group_member_create')(context, member_dict)
-        group_dict = _get_action('group_show')(context,
-                                               {'id': data['group_id']})
+    org_or_group = 'organization' if group.is_organization else 'group'
+    _get_action(f'{org_or_group}_member_create')(context, member_dict)
+    group_dict = _get_action(f'{org_or_group}_show')(
+        context, {'id': data['group_id']})
+
     try:
         mailer.send_invite(user, group_dict, data['role'])
     except (socket_error, mailer.MailerException) as error:
diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -441,6 +441,13 @@ def default_user_schema(
     })
 
 
+@validator_args
+def create_user_for_user_invite_schema(ignore_missing: Validator):
+    schema = default_user_schema()
+    schema['password'] = [ignore_missing]
+    return schema
+
+
 @validator_args
 def user_new_form_schema(
         unicode_safe: Validator, user_both_passwords_entered: Validator,


Test patch:
diff --git a/ckan/tests/controllers/test_organization.py b/ckan/tests/controllers/test_organization.py
--- a/ckan/tests/controllers/test_organization.py
+++ b/ckan/tests/controllers/test_organization.py
@@ -5,6 +5,7 @@
 from bs4 import BeautifulSoup
 
 import ckan.authz as authz
+import ckan.model as model
 from ckan.lib.helpers import url_for
 from ckan.tests import factories, helpers
 
@@ -576,6 +577,24 @@ def test_anonymous_users_cannot_add_members(self, app):
                 status=403,
             )
 
+    def test_create_user_for_user_invite(self, mail_server):
+        group = factories.Group()
+        sysadmin = factories.Sysadmin()
+        context = {"user": sysadmin["name"]}
+
+        user_form = {
+            "email": "user@ckan.org",
+            "group_id": group["id"],
+            "role": "member"
+        }
+
+        user_dict = helpers.call_action("user_invite", context, **user_form)
+        user_obj = model.User.get(user_dict["id"])
+
+        assert user_obj.password is None
+        assert user_obj.state == 'pending'
+        assert user_obj.last_active is None
+
     def test_member_delete(self, app):
         sysadmin = factories.Sysadmin()
         user = factories.User()