From 46dea95c119e590c73047c20399b81a516375c20 Mon Sep 17 00:00:00 2001
From: Brian Coca <briancoca+dev@gmail.com>
Date: Sun, 21 Jul 2013 11:18:31 -0400
Subject: [PATCH 1/6] initial draft acl module

Signed-off-by: Brian Coca <briancoca+dev@gmail.com>
---
 files/acl | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 156 insertions(+)
 create mode 100644 files/acl

diff --git a/files/acl b/files/acl
new file mode 100644
index 00000000000..6ba1047b7e2
--- /dev/null
+++ b/files/acl
@@ -0,0 +1,156 @@
+#!/usr/bin/python
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+
+DOCUMENTATION = '''
+---
+module: acl
+version_added: "1.3"
+short_description: set and retrieve file acl
+description:
+     - Sets and retrivies acl for a file
+options:
+  name:
+    required: true
+    default: None
+    description:
+      - The full path of the file/object to get the facts of
+    aliases: ['path']
+  entry:
+    required: false
+    default: None
+    description:
+      - The acl to set/remove. MUST always quote! In form of '<type>:<qualifier>:<perms>', qualifier may be empty for some types but type and perms are always requried. '-' can be used as placeholder when you don't care about permissions.
+  state:
+    required: false
+    default: get
+    choices: [ 'get', 'present', 'absent' ]
+    description:
+      - defines which operation you want to do. C(get) get the current acl C(present) sets/changes the acl, requires entry field C(absent) deletes the acl, requires entry field
+  follow:
+    required: false
+    default: yes
+    choices: [ 'yes', 'no' ]
+    description:
+      - if yes, dereferences symlinks and sets/gets attributes on symlink target, otherwise acts on symlink itself.
+author: Brian Coca
+notes:
+    - The "acl" module requires that acl is enabled on the target filesystem and that the setfacl and getfacl binaries are installed.
+'''
+
+EXAMPLES = '''
+# Obtain the acl of /etc/foo.conf
+- acl: name=/etc/foo.conf
+
+# Grants joe read access to foo
+- acl: name=/etc/foo.conf entry="u:joe:r" state=present
+
+# Removes the acl for joe
+- acl: name=/etc/foo.conf entry="u:joe" state=absent
+'''
+
+try:
+    import posix1e
+except:
+    module.fail_json(msg="Could not import required module pylibacl (posix1e)")
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            name = dict(required=True,aliases=['path']),
+            entry = dict(required=False, default=None),
+            state = dict(required=False, default='get', choices=[ 'get', 'present', 'absent' ], type='str'),
+            follow = dict(required=False, type='bool', default=True),
+        ),
+        supports_check_mode=True,
+    )
+    path = module.params.get('name')
+    entry = module.params.get('entry')
+    state = module.params.get('state')
+    follow = module.params.get('follow')
+
+    if not os.path.exists(path):
+        module.fail_json(msg="path not found or not accessible!")
+
+    if entry is None and state in ['present','absent']:
+        module.fail_json(msg="%s needs entry to be set" % state)
+
+    if entry.count(":") != 3:
+        module.fail_json(msg="Invalid entry: '%s', it requires 3 sections divided by ':'" % entry)
+
+    changed=False
+    changes=0
+    msg = ""
+    currentacl = posix1e.ACL(file=path)
+    newacl = currentacl
+    res = currentacl
+
+    if (state == 'present'):
+        for newe in posix1e.ACL(text=entry):
+            matched = False
+            for olde in currentacl:
+                diff = False
+                if olde.tag_type == newe.tag_type:
+                    if newe.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]:
+                        if olde.qualifier == newe.qualifier:
+                            matched = True
+                            if not str(olde.permset) == str(newe.permset):
+                                diff = True
+                    else:
+                        matched = True
+                        if not str(olde.permset) == str(newe.permset):
+                            diff = True
+                if diff:
+                    newacl.delete_entry(olde)
+                    newacl.append(newe)
+                    changes=changes+1
+                if matched:
+                    break
+            if not matched:
+                newacl.append(newe)
+                changes=changes+1
+        msg="%s is present" % (entry)
+    elif state == 'absent':
+        for rme in posix1e.ACL(text=entry):
+            for olde in currentacl:
+                if olde.tag_type == rme.tag_type:
+                    if rme.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]:
+                        if olde.qualifier == rme.qualifier:
+                            newacl.delete_entry(olde)
+                            changes=changes+1
+                            break
+                    else:
+                        newacl.delete_entry(olde)
+                        changes=changes+1
+                        break
+        msg="%s is absent" % (entry)
+    else:
+        msg="current acl"
+
+    if changes > 0:
+        if not newacl.valid():
+            module.fail_json("Invalid acl constructed: %s" % newacl.to_any_text())
+        if not module.check_mode:
+            newacl.applyto(path)
+        changed=True
+        res=newacl
+
+    msg="%s. %d entries changed" % (msg,changes)
+    module.exit_json(changed=changed, msg=msg, acl=res.to_any_text().split())
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+
+main()

From 9bb2e6a63e397932caf27c7d3eeb14ff11974d36 Mon Sep 17 00:00:00 2001
From: Brian Coca <briancoca+dev@gmail.com>
Date: Sun, 21 Jul 2013 11:32:56 -0400
Subject: [PATCH 2/6] corrected absent example Signed-off-by: Brian Coca
 <briancoca+dev@gmail.com>

---
 files/acl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/files/acl b/files/acl
index 6ba1047b7e2..a9a46bbb029 100644
--- a/files/acl
+++ b/files/acl
@@ -58,7 +58,7 @@ EXAMPLES = '''
 - acl: name=/etc/foo.conf entry="u:joe:r" state=present
 
 # Removes the acl for joe
-- acl: name=/etc/foo.conf entry="u:joe" state=absent
+- acl: name=/etc/foo.conf entry="u:joe:-" state=absent
 '''
 
 try:

From a2226228d93d14a578737477f7139e08043344a6 Mon Sep 17 00:00:00 2001
From: Brian Coca <briancoca+dev@gmail.com>
Date: Thu, 25 Jul 2013 21:51:29 -0400
Subject: [PATCH 3/6] fixed error on detecting missing requirements
 Signed-off-by: Brian Coca <briancoca+dev@gmail.com>

---
 files/acl | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/files/acl b/files/acl
index a9a46bbb029..a95a032c93f 100644
--- a/files/acl
+++ b/files/acl
@@ -60,11 +60,11 @@ EXAMPLES = '''
 # Removes the acl for joe
 - acl: name=/etc/foo.conf entry="u:joe:-" state=absent
 '''
-
+NO_PYLIBACL=False
 try:
     import posix1e
 except:
-    module.fail_json(msg="Could not import required module pylibacl (posix1e)")
+    NO_PYLIBACL=True
 
 def main():
     module = AnsibleModule(
@@ -76,6 +76,10 @@ def main():
         ),
         supports_check_mode=True,
     )
+
+    if NO_PYLIBACL:
+        module.fail_json(msg="Could not import required module pylibacl (posix1e)")
+
     path = module.params.get('name')
     entry = module.params.get('entry')
     state = module.params.get('state')

From 73ffb0c0202e239e7216737cbc9adc31fa6a3864 Mon Sep 17 00:00:00 2001
From: Brian Coca <briancoca+dev@gmail.com>
Date: Thu, 22 Aug 2013 23:35:24 -0400
Subject: [PATCH 4/6] - fixed typos and errors from feedback - now makes sure a
 proper mask is added - now captures I/O error produced when group, user or
 permissions are   invalid Signed-off-by: Brian Coca <briancoca+dev@gmail.com>

---
 files/acl | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/files/acl b/files/acl
index a95a032c93f..57dbb838ae1 100644
--- a/files/acl
+++ b/files/acl
@@ -47,7 +47,7 @@ options:
       - if yes, dereferences symlinks and sets/gets attributes on symlink target, otherwise acts on symlink itself.
 author: Brian Coca
 notes:
-    - The "acl" module requires that acl is enabled on the target filesystem and that the setfacl and getfacl binaries are installed.
+    - The "acl" module requires the posix1e module on the target machine and  that acl is enabled on the target filesystem.
 '''
 
 EXAMPLES = '''
@@ -66,6 +66,12 @@ try:
 except:
     NO_PYLIBACL=True
 
+def gen_acl(module,entry):
+    try:
+        return posix1e.ACL(text=entry)
+    except IOError, e:
+        module.fail_json(msg="Invalid entry: '%s', check that user/groups exist and permissions are correct" % entry)
+
 def main():
     module = AnsibleModule(
         argument_spec = dict(
@@ -91,7 +97,7 @@ def main():
     if entry is None and state in ['present','absent']:
         module.fail_json(msg="%s needs entry to be set" % state)
 
-    if entry.count(":") != 3:
+    if entry.count(":") != 2:
         module.fail_json(msg="Invalid entry: '%s', it requires 3 sections divided by ':'" % entry)
 
     changed=False
@@ -101,8 +107,9 @@ def main():
     newacl = currentacl
     res = currentacl
 
+
     if (state == 'present'):
-        for newe in posix1e.ACL(text=entry):
+        for newe in gen_acl(module, entry):
             matched = False
             for olde in currentacl:
                 diff = False
@@ -127,7 +134,7 @@ def main():
                 changes=changes+1
         msg="%s is present" % (entry)
     elif state == 'absent':
-        for rme in posix1e.ACL(text=entry):
+        for rme in gen_acl(module, entry):
             for olde in currentacl:
                 if olde.tag_type == rme.tag_type:
                     if rme.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]:
@@ -144,8 +151,9 @@ def main():
         msg="current acl"
 
     if changes > 0:
+        newacl.calc_mask()
         if not newacl.valid():
-            module.fail_json("Invalid acl constructed: %s" % newacl.to_any_text())
+            module.fail_json(msg="Invalid acl constructed: %s" % newacl.to_any_text())
         if not module.check_mode:
             newacl.applyto(path)
         changed=True

From 1ddcc9574bffdc193576d09c6fa1f36b4393c673 Mon Sep 17 00:00:00 2001
From: Brian Coca <briancoca+dev@gmail.com>
Date: Tue, 10 Sep 2013 23:13:36 -0400
Subject: [PATCH 5/6] now w/o python module dependencies

Signed-off-by: Brian Coca <briancoca+dev@gmail.com>
---
 files/acl | 157 ++++++++++++++++++++++++++++++++----------------------
 1 file changed, 93 insertions(+), 64 deletions(-)

diff --git a/files/acl b/files/acl
index 57dbb838ae1..458a49dbd6f 100644
--- a/files/acl
+++ b/files/acl
@@ -35,10 +35,10 @@ options:
       - The acl to set/remove. MUST always quote! In form of '<type>:<qualifier>:<perms>', qualifier may be empty for some types but type and perms are always requried. '-' can be used as placeholder when you don't care about permissions.
   state:
     required: false
-    default: get
-    choices: [ 'get', 'present', 'absent' ]
+    default: query
+    choices: [ 'query', 'present', 'absent' ]
     description:
-      - defines which operation you want to do. C(get) get the current acl C(present) sets/changes the acl, requires entry field C(absent) deletes the acl, requires entry field
+      - defines which operation you want to do. C(query) get the current acl C(present) sets/changes the acl, requires permissions field C(absent) deletes the acl, requires permissions field
   follow:
     required: false
     default: yes
@@ -47,7 +47,10 @@ options:
       - if yes, dereferences symlinks and sets/gets attributes on symlink target, otherwise acts on symlink itself.
 author: Brian Coca
 notes:
-    - The "acl" module requires the posix1e module on the target machine and  that acl is enabled on the target filesystem.
+    - The "acl" module requires that acl is enabled on the target filesystem and that the setfacl and getfacl binaries are installed.
+author: Brian Coca
+notes:
+    - The "acl" module requires the acl command line utilities be installed on the target machine and  that acl is enabled on the target filesystem.
 '''
 
 EXAMPLES = '''
@@ -55,37 +58,64 @@ EXAMPLES = '''
 - acl: name=/etc/foo.conf
 
 # Grants joe read access to foo
-- acl: name=/etc/foo.conf entry="u:joe:r" state=present
+- acl: name=/etc/foo.conf entry="user:joe:r" state=present
 
 # Removes the acl for joe
-- acl: name=/etc/foo.conf entry="u:joe:-" state=absent
+- acl: name=/etc/foo.conf entry="user:joe:-" state=absent
 '''
-NO_PYLIBACL=False
-try:
-    import posix1e
-except:
-    NO_PYLIBACL=True
 
-def gen_acl(module,entry):
+def get_acl(module,path,entry,follow):
+
+    cmd = [ module.get_bin_path('getfacl', True) ]
+    if not follow:
+        cmd.append('-h')
+    # prevents absolute path warnings and removes headers
+    cmd.append('-cp')
+    cmd.append(path)
+
+    return _run_acl(module,cmd)
+
+def set_acl(module,path,entry,follow):
+
+    cmd = [ module.get_bin_path('setfacl', True) ]
+    if not follow:
+        cmd.append('-h')
+    cmd.append('-m "%s"' % entry)
+    cmd.append(path)
+
+    return _run_acl(module,cmd)
+
+def rm_acl(module,path,entry,follow):
+
+    cmd = [ module.get_bin_path('setfacl', True) ]
+    if not follow:
+        cmd.append('-h')
+    entry = entry[0:entry.rfind(':')]
+    cmd.append('-x "%s"' % entry)
+    cmd.append(path)
+
+    return _run_acl(module,cmd,False)
+
+def _run_acl(module,cmd,check_rc=True):
+
     try:
-        return posix1e.ACL(text=entry)
-    except IOError, e:
-        module.fail_json(msg="Invalid entry: '%s', check that user/groups exist and permissions are correct" % entry)
+        (rc, out, err) = module.run_command(' '.join(cmd), check_rc=check_rc)
+    except Exception, e:
+        module.fail_json(msg=e.strerror)
+
+    return out.splitlines()
 
 def main():
     module = AnsibleModule(
         argument_spec = dict(
             name = dict(required=True,aliases=['path']),
             entry = dict(required=False, default=None),
-            state = dict(required=False, default='get', choices=[ 'get', 'present', 'absent' ], type='str'),
+            state = dict(required=False, default='query', choices=[ 'query', 'present', 'absent' ], type='str'),
             follow = dict(required=False, type='bool', default=True),
         ),
         supports_check_mode=True,
     )
 
-    if NO_PYLIBACL:
-        module.fail_json(msg="Could not import required module pylibacl (posix1e)")
-
     path = module.params.get('name')
     entry = module.params.get('entry')
     state = module.params.get('state')
@@ -94,73 +124,72 @@ def main():
     if not os.path.exists(path):
         module.fail_json(msg="path not found or not accessible!")
 
-    if entry is None and state in ['present','absent']:
-        module.fail_json(msg="%s needs entry to be set" % state)
-
-    if entry.count(":") != 2:
-        module.fail_json(msg="Invalid entry: '%s', it requires 3 sections divided by ':'" % entry)
+    if entry is None:
+        if state in ['present','absent']:
+            module.fail_json(msg="%s needs entry to be set" % state)
+    else:
+        if entry.count(":") != 2:
+            module.fail_json(msg="Invalid entry: '%s', it requires 3 sections divided by ':'" % entry)
 
     changed=False
     changes=0
     msg = ""
-    currentacl = posix1e.ACL(file=path)
-    newacl = currentacl
-    res = currentacl
+    currentacl = get_acl(module,path,entry,follow)
 
 
     if (state == 'present'):
-        for newe in gen_acl(module, entry):
-            matched = False
-            for olde in currentacl:
-                diff = False
-                if olde.tag_type == newe.tag_type:
-                    if newe.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]:
-                        if olde.qualifier == newe.qualifier:
-                            matched = True
-                            if not str(olde.permset) == str(newe.permset):
-                                diff = True
-                    else:
+        newe = entry.split(':')
+        matched = False
+        for oldentry in currentacl:
+            diff = False
+            olde = oldentry.split(':')
+            if olde[0] == newe[0]:
+                if newe[0] in ['user', 'group']:
+                    if olde[1] == newe[1]:
                         matched = True
-                        if not str(olde.permset) == str(newe.permset):
+                        if not olde[2] == newe[2]:
                             diff = True
-                if diff:
-                    newacl.delete_entry(olde)
-                    newacl.append(newe)
-                    changes=changes+1
-                if matched:
-                    break
-            if not matched:
-                newacl.append(newe)
+                else:
+                    matched = True
+                    if not olde[2] == newe[2]:
+                        diff = True
+            if diff:
                 changes=changes+1
+                if not module.check_mode:
+                    set_acl(module,path,entry,follow)
+            if matched:
+                break
+        if not matched:
+            changes=changes+1
+            if not module.check_mode:
+                set_acl(module,path,entry,follow)
         msg="%s is present" % (entry)
     elif state == 'absent':
-        for rme in gen_acl(module, entry):
-            for olde in currentacl:
-                if olde.tag_type == rme.tag_type:
-                    if rme.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]:
-                        if olde.qualifier == rme.qualifier:
-                            newacl.delete_entry(olde)
-                            changes=changes+1
-                            break
-                    else:
-                        newacl.delete_entry(olde)
+        rme = entry.split(':')
+        for oldentry in currentacl:
+            olde = oldentry.split(':')
+            if olde[0] == rme[0]:
+                if rme[0] in ['user', 'group']:
+                    if olde[1] == rme[1]:
                         changes=changes+1
+                        if not module.check_mode:
+                            rm_acl(module,path,entry,follow)
                         break
+                else:
+                    changes=changes+1
+                    if not module.check_mode:
+                        rm_acl(module,path,entry,follow)
+                    break
         msg="%s is absent" % (entry)
     else:
         msg="current acl"
 
     if changes > 0:
-        newacl.calc_mask()
-        if not newacl.valid():
-            module.fail_json(msg="Invalid acl constructed: %s" % newacl.to_any_text())
-        if not module.check_mode:
-            newacl.applyto(path)
         changed=True
-        res=newacl
+        currentacl = get_acl(module,path,entry,follow)
 
     msg="%s. %d entries changed" % (msg,changes)
-    module.exit_json(changed=changed, msg=msg, acl=res.to_any_text().split())
+    module.exit_json(changed=changed, msg=msg, acl=currentacl)
 
 # this is magic, see lib/ansible/module_common.py
 #<<INCLUDE_ANSIBLE_MODULE_COMMON>>

From 6db8c642d53ea2bc8b6d64e2fbeb2b7c4eccae6c Mon Sep 17 00:00:00 2001
From: Brian Coca <briancoca+dev@gmail.com>
Date: Mon, 14 Oct 2013 10:48:30 -0400
Subject: [PATCH 6/6] added long names to support older version as per feedback
 Signed-off-by: Brian Coca <briancoca+dev@gmail.com>

---
 files/acl | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/files/acl b/files/acl
index 458a49dbd6f..1b50d21c58f 100644
--- a/files/acl
+++ b/files/acl
@@ -70,7 +70,8 @@ def get_acl(module,path,entry,follow):
     if not follow:
         cmd.append('-h')
     # prevents absolute path warnings and removes headers
-    cmd.append('-cp')
+    cmd.append('--omit-header')
+    cmd.append('--absolute-names')
     cmd.append(path)
 
     return _run_acl(module,cmd)