Repository: ansible/ansible-lint

Issue:
E202 (no leading zero for octal permission) False positive
# Issue Type
- Bug report

# Ansible and Ansible Lint details

- ansible-lint installation method: Ansible Galaxy use of ansible-lint

# Desired Behaviour

ansible-lint provides correct reason to following the lint

# Actual Behaviour (Bug report only)

Please give some details of what is actually happening.
Include a [minimum complete verifiable example](http://stackoverflow.com/help/mcve)
with:
- playbook
- output of running ansible-lint
- if you're getting a stack trace, output of
  `ansible-playbook --syntax-check playbook`

"Numeric file permissions without leading zero can behave in unexpected ways. See http://docs.ansible.com/ansible/file_module.html" - https://docs.ansible.com/ansible-lint/rules/default_rules.html

./galaxy/downloads/tmp2REDBx/tasks/main-tasks.yml:4: [E202] Octal file permissions must contain leading zero

```
# Lets create the configuration first...
# avoid locking ourself out.

- name: Create firewalld config directories
  file:
    dest: "{{ item }}"
    state: directory
    mode: "750"
  with_items:
    - /etc/firewalld
    - /etc/firewalld/zones
  notify: firewalld__reload
```

It is not correct that "750" is mis-interpreted. "750" is a string, and is correctly interpreted as the octal permission number `750` i.e. `rwxr-x---`.

Personally I have been using the string "750", rather than remember that YAML supports octal numeric literals using a leading zero.


Discussion:
Rule 202 should be edited to check for quotes or a leading zero, as specified in the file module doc. Thanks for the catch!
https://github.com/ansible/ansible-lint/blob/991079d5a865c5fb4a6dd05242b5058cb57bff41/lib/ansiblelint/rules/OctalPermissionsRule.py#L26-L33

Resolving patch:
diff --git a/lib/ansiblelint/rules/OctalPermissionsRule.py b/lib/ansiblelint/rules/OctalPermissionsRule.py
--- a/lib/ansiblelint/rules/OctalPermissionsRule.py
+++ b/lib/ansiblelint/rules/OctalPermissionsRule.py
@@ -19,13 +19,12 @@
 # THE SOFTWARE.
 
 from ansiblelint import AnsibleLintRule
-import re
 import six
 
 
 class OctalPermissionsRule(AnsibleLintRule):
     id = '202'
-    shortdesc = 'Octal file permissions must contain leading zero'
+    shortdesc = 'Octal file permissions must contain leading zero or be a string'
     description = (
         'Numeric file permissions without leading zero can behave '
         'in unexpected ways. See '
@@ -38,9 +37,6 @@ class OctalPermissionsRule(AnsibleLintRule):
     _modules = ['assemble', 'copy', 'file', 'ini_file', 'lineinfile',
                 'replace', 'synchronize', 'template', 'unarchive']
 
-    mode_regex = re.compile(r'^\s*[0-9]+\s*$')
-    valid_mode_regex = re.compile(r'^\s*0[0-7]{3,4}\s*$')
-
     def is_invalid_permission(self, mode):
         # sensible file permission modes don't
         # have write bit set when read bit is
@@ -70,7 +66,9 @@ def is_invalid_permission(self, mode):
     def matchtask(self, file, task):
         if task["action"]["__ansible_module__"] in self._modules:
             mode = task['action'].get('mode', None)
-            if isinstance(mode, six.string_types) and self.mode_regex.match(mode):
-                return not self.valid_mode_regex.match(mode)
+
+            if isinstance(mode, six.string_types):
+                return False
+
             if isinstance(mode, int):
                 return self.is_invalid_permission(mode)


Test patch:
diff --git a/test/TestOctalPermissions.py b/test/TestOctalPermissions.py
--- a/test/TestOctalPermissions.py
+++ b/test/TestOctalPermissions.py
@@ -1,6 +1,77 @@
 import unittest
-from ansiblelint import Runner, RulesCollection
+
+from ansiblelint import RulesCollection
 from ansiblelint.rules.OctalPermissionsRule import OctalPermissionsRule
+from test import RunFromText
+
+SUCCESS_TASKS = '''
+---
+- hosts: hosts
+  vars:
+    varset: varset
+  tasks:
+    - name: octal permissions test success (0600)
+      file:
+        path: foo
+        mode: 0600
+
+    - name: octal permissions test success (0000)
+      file:
+        path: foo
+        mode: 0000
+
+    - name: octal permissions test success (02000)
+      file:
+        path: bar
+        mode: 02000
+
+    - name: octal permissions test success (02751)
+      file:
+        path: bar
+        mode: 02751
+
+    - name: octal permissions test success (0777)
+      file: path=baz mode=0777
+
+    - name: octal permissions test success (0711)
+      file: path=baz mode=0711
+
+    - name:  permissions test success (0777)
+      file: path=baz mode=u+rwx
+
+    - name: octal permissions test success (777)
+      file: path=baz mode=777
+
+    - name: octal permissions test success (733)
+      file: path=baz mode=733
+'''
+
+FAIL_TASKS = '''
+---
+- hosts: hosts
+  vars:
+    varset: varset
+  tasks:
+    - name: octal permissions test fail (600)
+      file:
+        path: foo
+        mode: 600
+
+    - name: octal permissions test fail (710)
+      file:
+        path: foo
+        mode: 710
+
+    - name: octal permissions test fail (123)
+      file:
+        path: foo
+        mode: 123
+
+    - name: octal permissions test fail (2000)
+      file:
+        path: bar
+        mode: 2000
+'''
 
 
 class TestOctalPermissionsRuleWithFile(unittest.TestCase):
@@ -19,17 +90,15 @@ class TestOctalPermissionsRuleWithFile(unittest.TestCase):
     def setUp(self):
         self.rule = OctalPermissionsRule()
         self.collection.register(self.rule)
+        self.runner = RunFromText(self.collection)
 
-    def test_file_positive(self):
-        success = 'test/octalpermissions-success.yml'
-        good_runner = Runner(self.collection, success, [], [], [])
-        self.assertEqual([], good_runner.run())
+    def test_success(self):
+        results = self.runner.run_playbook(SUCCESS_TASKS)
+        self.assertEqual(0, len(results))
 
-    def test_file_negative(self):
-        failure = 'test/octalpermissions-failure.yml'
-        bad_runner = Runner(self.collection, failure, [], [], [])
-        errs = bad_runner.run()
-        self.assertEqual(5, len(errs))
+    def test_fail(self):
+        results = self.runner.run_playbook(FAIL_TASKS)
+        self.assertEqual(4, len(results))
 
     def test_valid_modes(self):
         for mode in self.VALID_MODES:
diff --git a/test/octalpermissions-failure.yml b/test/octalpermissions-failure.yml
deleted file mode 100644
--- a/test/octalpermissions-failure.yml
+++ /dev/null
@@ -1,30 +0,0 @@
----
-- hosts: hosts
-  vars:
-    varset: varset
-  tasks:
-    - name: octal permissions test fail (600)
-      file:
-        path: foo
-        mode: 600
-
-    - name: octal permissions test fail (710)
-      file:
-        path: foo
-        mode: 710
-
-    - name: octal permissions test fail (123)
-      file:
-        path: foo
-        mode: 123
-
-    - name: octal permissions test fail (2000)
-      file:
-        path: bar
-        mode: 2000
-
-    - name: octal permissions test fail (777)
-      file: path=baz mode=777
-
-    - name: octal permissions test fail (0733)
-      file: path=baz mode=0733
diff --git a/test/octalpermissions-success.yml b/test/octalpermissions-success.yml
deleted file mode 100644
--- a/test/octalpermissions-success.yml
+++ /dev/null
@@ -1,36 +0,0 @@
----
-- hosts: hosts
-  vars:
-    varset: varset
-  tasks:
-    - name: octal permissions test success (0600)
-      file:
-        path: foo
-        mode: 0600
-
-    - name: octal permissions test success (0000)
-      file:
-        path: foo
-        mode: 0000
-
-    - name: octal permissions test success (02000)
-      file:
-        path: bar
-        mode: 02000
-
-    - name: octal permissions test success (02751)
-      file:
-        path: bar
-        mode: 02751
-
-    - name: octal permissions test success (0777)
-      file: path=baz mode=0777
-
-    - name: octal permissions test success (0711)
-      file: path=baz mode=0711
-
-    - name: octal permissions test success (0710)
-      file: path=baz mode=0710
-
-    - name:  permissions test success (0777)
-      file: path=baz mode=u+rwx