Repository: saltstack/salt
Pull request: Option to update htpasswd only if changed
### What does this PR do?

Adds a boolean flag called `update` to `webutil.user_exists` to make it update a user's password if it has changed.
### What issues does this PR fix or reference?

Fixes #35445
### Previous Behavior

The file is not updated if the user exists in the file at all. There exists a `force` option, but that updates the file regardless of whether the password has changed or not, which makes it difficult to have other things happen on changes to this, like bouncing nginx or whatnot. So previously, you could either never update the file, or always update the file.
### New Behavior

The file (and state) changes if the password input is different from what's in the file.
### Tests written?

No

Please review [Salt's Contributing Guide](https://docs.saltstack.com/en/latest/topics/development/contributing.html) for best practices.


Problem statement:
webutils.user_exits does not enforce password updates
### Description of Issue/Question

The webutils user_exists feature doesn't appear to enforce the password for a htpasswd user.
It will ensure it exists but will not update the password if the source password value changes.
If i use the force=True option it changes the file every run which is also not desirable.. 
As this will report changes when there shouldn't be any.
### Setup

SLS file:
{% for user, pass in salt.pillar.get('web:users').iteritems() %}
{{ user }}:
  webutil.user_exists:
    - password: {{ pass }}
    - htpasswd_file: /etc/st2/htpasswd
    - options: d

{% endfor %}

Pillar:
users:
    st2admin:
      test
    st2user:
      vegetable
### Steps to Reproduce Issue

Use the webutils.user_exists feature to add a users to a htpasswd file.
Change the password and then re run state file.
The password is not checked/revalidate/enforced to the best i can see
### Versions Report

Master:
Salt Version:
           Salt: 2015.8.8.2

Dependency Versions:
         Jinja2: unknown
       M2Crypto: Not Installed
           Mako: Not Installed
         PyYAML: 3.11
          PyZMQ: 14.5.0
         Python: 2.6.6 (r266:84292, Jul 23 2015, 15:22:56)
           RAET: Not Installed
        Tornado: 4.2.1
            ZMQ: 4.0.5
           cffi: Not Installed
       cherrypy: Not Installed
       dateutil: Not Installed
          gitdb: 0.5.4
      gitpython: 0.3.2 RC1
          ioflo: Not Installed
        libgit2: Not Installed
        libnacl: Not Installed
   msgpack-pure: Not Installed
 msgpack-python: 0.4.6
   mysql-python: Not Installed
      pycparser: Not Installed
       pycrypto: 2.6.1
         pygit2: Not Installed
   python-gnupg: Not Installed
          smmap: 0.8.1
        timelib: Not Installed

System Versions:
           dist: centos 6.7 Final
        machine: x86_64
        release: 2.6.32-573.12.1.el6.x86_64
         system: CentOS 6.7 Final

minion: 
[root@salt Debian]# salt 'INS-4132' test.version
INS-4132:
    2016.3.1
Ubuntu 14.04

Hints from issue discussion:
@paul-mulvihill looks like you are correct. Glancing at the code [here](https://github.com/saltstack/salt/blob/develop/salt/states/htpasswd.py#L62) It just checks to see if the user already exists in the htpasswd file. If user exists then it does not update the information.

@paul-mulvihill @Ch3LL 
I think if you apply
`force: True`
the htpasswd file will be updated even if the user already exists, as you can see from the explanation:

```
    force
        Touch the file even if user already created
```

So, I guess the behavior that @paul-mulvihill experienced is by design. 
For more details, you can check [here](https://github.com/saltstack/salt/blob/develop/salt/states/htpasswd.py#L32-L54)

Hi @hu-dabao 
Yeah i'm aware of the force option, i mention it in my original post. My concern with using that is that it will apply a change EVERY run rather than when it is needed. 
If i have the highstate applying on a scheduled and the logs being passed into my logging infrastructure it will be reporting changes being applied when there is no need for them to be. Which makes any reporting inaccurate. 
I'm after finding out if this is the intended end design for this state/module functionality. if not are there plans to make it so passwords can be managed/maintained without forcing a change every run.
if this is the intended end design of the feature I may need to review how I use it to manage the htpaswd file. I don't want salt to apply a change if there isn't one required as it will skew any reporting I have to report drift.

Commit message: Option to update htpasswd only if changed

Patch to review:
diff --git a/salt/modules/htpasswd.py b/salt/modules/htpasswd.py
index 4114b895761f..6a35db05277f 100644
--- a/salt/modules/htpasswd.py
+++ b/salt/modules/htpasswd.py
@@ -139,3 +139,46 @@ def userdel(pwfile, user, runas=None):
     out = __salt__['cmd.run'](cmd, runas=runas,
                               python_shell=False).splitlines()
     return out
+
+
+def verify(pwfile, user, password, opts='', runas=None):
+    '''
+    Return True if the htpasswd file exists, the user has an entry, and their
+    password matches.
+
+    pwfile
+        Path to htpasswd file
+
+    user
+        User name
+
+    password
+        User password
+
+    opts
+        Valid options that can be passed are:
+
+            - `m`  Force MD5 encryption of the password (default).
+            - `d`  Force CRYPT encryption of the password.
+            - `p`  Do not encrypt the password (plaintext).
+            - `s`  Force SHA encryption of the password.
+
+    runas
+        The system user to run htpasswd command with
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' webutil.verify /etc/httpd/htpasswd larry maybepassword
+        salt '*' webutil.verify /etc/httpd/htpasswd larry maybepassword opts=ns
+    '''
+    if not os.path.exists(pwfile):
+        return False
+
+    cmd = ['htpasswd', '-bv{0}'.format(opts), pwfile, user, password]
+    ret = __salt__['cmd.run_all'](cmd, runas=runas, python_shell=False)
+    log.debug('Result of verifying htpasswd for user {0}: {1}'.format(
+        user, ret))
+
+    return ret['retcode'] == 0
diff --git a/salt/states/htpasswd.py b/salt/states/htpasswd.py
index 3200be6f8ab8..423406e4666c 100644
--- a/salt/states/htpasswd.py
+++ b/salt/states/htpasswd.py
@@ -30,7 +30,7 @@ def __virtual__():
 
 
 def user_exists(name, password=None, htpasswd_file=None, options='',
-                force=False, runas=None):
+                force=False, runas=None, update=False):
     '''
     Make sure the user is inside the specified htpasswd file
 
@@ -52,15 +52,26 @@ def user_exists(name, password=None, htpasswd_file=None, options='',
     runas
         The system user to run htpasswd command with
 
+    update
+        Update an existing user's password if it's different from what's in
+        the htpasswd file (unlike force, which updates regardless)
+
     '''
     ret = {'name': name,
            'changes': {},
            'comment': '',
            'result': None}
 
-    grep = __salt__['file.grep']
-    grep_ret = grep(htpasswd_file, name)
-    if grep_ret['retcode'] != 0 or force:
+    exists = __salt__['file.grep'](htpasswd_file, name)['retcode'] == 0
+
+    # If user exists, but we're supposed to update the password, find out if
+    # it's changed, but not if we're forced to update the file regardless.
+    password_changed = False
+    if exists and update and not force:
+        password_changed = not __salt__['webutil.verify'](
+            htpasswd_file, name, password, opts=options, runas=runas)
+
+    if not exists or password_changed or force:
         if __opts__['test']:
             ret['result'] = None
             ret['comment'] = ('User \'{0}\' is set to be added to htpasswd '


Code review comments:
diff_hunk: @@ -139,3 +139,46 @@ def userdel(pwfile, user, runas=None):
     out = __salt__['cmd.run'](cmd, runas=runas,
                               python_shell=False).splitlines()
     return out
+
+
+def verify(pwfile, user, password, opts='', runas=None):
+    '''
+    Return True if the htpasswd file exists, the user has an entry, and their
+    password matches.
+
+    pwfile
+        Path to htpasswd file
path: salt/modules/htpasswd.py
text: @user1:
We should specify that this must be the fully-qualified path to the file, just so there is no confusion. Sometimes people try to assume a relative location. (Yes, I know that's a little weird but it happens.) ;]