Repository: ansible/ansible-modules-core

Issue:
Ansible is unable to configure multiple permitopen authorized_key options
The ansible documentation states that:

> ```
>  A string of ssh key options to be prepended to the key in the authorized_keys file
> ```

source: http://docs.ansible.com/authorized_key_module.html

The ssh documentation states:

> permitopen="host:port"
>    Limit local port forwarding with ssh(1) -L such that it may only connect to the specified host and port. IPv6 addresses can be specified by enclosing the address in square brackets. **Multiple permitopen options may be applied separated by commas.** No pattern matching is performed on the specified hostnames, they must be literal domains or addresses. A port specification of \* matches any port.

With the above in mind I'm testing the following ansible rule:

``` yaml

  authorized_key: user={{ item }}
                  key_options='no-agent-forwarding,no-X11-forwarding,permitopen="10.9.8.1:8080",permitopen="10.9.8.1:9001",permitopen="10.9.8.1:7767",permitopen="10.9.8.1:9900",permitopen="localhost:3001",permitopen="localhost:3002",permitopen="localhost:3003"'
                  key="{{ lookup('file', 'public_keys/' + item + '.pub') }}"
                  path="/home/{{ item }}/.ssh/authorized_keys"
                  manage_dir=no
                  state=present
```

This results in a key being added but with subsequent permitopen options being overwritten by the last parsed value:

```
root@build:~# cat /home/mulander/.ssh/authorized_keys 
no-agent-forwarding,no-X11-forwarding,permitopen="localhost:3003",permitopen="localhost:3003",permitopen="localhost:3003",permitopen="localhost:3003",permitopen="localhost:3003",permitopen="localhost:3003",permitopen="localhost:3003" ssh-rsa AAA
```

Please notice that all hosts and ports were replaced by `localhost:3003` which is the last `permitopen` entry. This overwrites both the host & port. The issue is most probably caused by parsing the values into a `dict` using the `permitopen` as the key. The value should be kept as an array in order not to loose configuration.

Tested on ansible 1.7.2

This was initially reported as [#11559 on the ansible main repository](https://github.com/ansible/ansible/issues/11559)



Discussion:

Resolving patch:
diff --git a/system/authorized_key.py b/system/authorized_key.py
--- a/system/authorized_key.py
+++ b/system/authorized_key.py
@@ -80,13 +80,13 @@
     version_added: "1.9"
   validate_certs:
     description:
-      - This only applies if using a https url as the source of the keys. If set to C(no), the SSL certificates will not be validated. 
-      - This should only set to C(no) used on personally controlled sites using self-signed certificates as it avoids verifying the source site. 
+      - This only applies if using a https url as the source of the keys. If set to C(no), the SSL certificates will not be validated.
+      - This should only set to C(no) used on personally controlled sites using self-signed certificates as it avoids verifying the source site.
       - Prior to 2.1 the code worked as if this was set to C(yes).
     required: false
     default: "yes"
     choices: ["yes", "no"]
-    version_added: "2.1" 
+    version_added: "2.1"
 author: "Ansible Core Team"
 '''
 
@@ -161,7 +161,7 @@ def __setitem__(self, key, value):
     def __iter__(self):
         return iter(self.itemlist)
     def keys(self):
-        return self.itemlist
+        return list(set(self.itemlist))
     def values(self):
         return [self[key] for key in self]
     def itervalues(self):
@@ -245,7 +245,13 @@ def parseoptions(module, options):
             for part in parts:
                 if "=" in part:
                     (key, value) = part.split("=", 1)
-                    options_dict[key] = value
+                    if options_dict.has_key(key):
+                        if isinstance(options_dict[key], list):
+                            options_dict[key].append(value)
+                        else:
+                            options_dict[key] = [options_dict[key], value]
+                    else:
+                        options_dict[key] = value
                 elif part != ",":
                     options_dict[part] = None
         except:
@@ -339,10 +345,13 @@ def writekeys(module, filename, keys):
                     option_strings = []
                     for option_key in options.keys():
                         if options[option_key]:
-                            option_strings.append("%s=%s" % (option_key, options[option_key]))
+                            if isinstance(options[option_key], list):
+                                for value in options[option_key]:
+                                    option_strings.append("%s=%s" % (option_key, value))
+                            else:
+                                option_strings.append("%s=%s" % (option_key, options[option_key]))
                         else:
                             option_strings.append("%s" % option_key)
-
                     option_str = ",".join(option_strings)
                     option_str += " "
                 key_line = "%s%s %s %s\n" % (option_str, type, keyhash, comment)


Test patch: