Repository: ansible/ansible-lint

Issue:
False positive detecting OctalPermissionsRule
When trying to setgid permissions

```
[ANSIBLE0008] Octal file permissions must contain leading zero
mode: 02775
```

Looks like the regex requires exactly 3 digits, which is not always correct.

```
# At least an indent, "mode:", optional whitespace, any digits, EOL
mode_regex = re.compile(r'^\s+mode:\s*[0-9]+\s*$')
# Same as above, but with a leading zero before three digits
valid_mode_regex = re.compile(r'^\s+mode:\s*0[0-7]{3}\s*$')
```



Discussion:
@siddharthist are you able to fix this up?

@willthames I can definitely work on this! I'll fix the regex and add a test case. 

@cblakkan Can you explain this a little more, please? Is the fix just to require 3 or 4 digits? Why are four digits valid? 

Pulled from manual pages for chmod. The first three rows illustrate the use for a 4th digit. 

```
Modes may be absolute or symbolic.  An absolute mode is an octal number constructed from the sum of one or more of the following values:

           4000    (the set-user-ID-on-execution bit) Executable files with this bit set will run with effective uid set to the uid of the file owner.  Directo-
                   ries with the set-user-id bit set will force all files and sub-directories created in them to be owned by the directory owner and not by the
                   uid of the creating process, if the underlying file system supports this feature: see chmod(2) and the suiddir option to mount(8).
           2000    (the set-group-ID-on-execution bit) Executable files with this bit set will run with effective gid set to the gid of the file owner.
           1000    (the sticky bit) See chmod(2) and sticky(8).
           0400    Allow read by owner.
           0200    Allow write by owner.
           0100    For files, allow execution by owner.  For directories, allow the owner to search in the directory.
           0040    Allow read by group members.
           0020    Allow write by group members.
           0010    For files, allow execution by group members.  For directories, allow group members to search in the directory.
           0004    Allow read by others.
           0002    Allow write by others.
           0001    For files, allow execution by others.  For directories allow others to search in the directory.
```

I believe this is the correct fix

```
valid_mode_regex = re.compile(r'^\s+mode:\s*0[0-7]{3,4}\s*$')
```

@cblakkan Thanks, I'll fix the regexp and add a test case!


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
@@ -33,7 +33,7 @@ class OctalPermissionsRule(AnsibleLintRule):
     # At least an indent, "mode:", optional whitespace, any digits, EOL
     mode_regex = re.compile(r'^\s+mode:\s*[0-9]+\s*$')
     # Same as above, but with a leading zero before three digits
-    valid_mode_regex = re.compile(r'^\s+mode:\s*0[0-7]{3}\s*$')
+    valid_mode_regex = re.compile(r'^\s+mode:\s*0[0-7]{3,4}\s*$')
 
     def match(self, file, line):
         if re.match(self.mode_regex, line):


Test patch:
diff --git a/test/TestOctalPermissions.py b/test/TestOctalPermissions.py
--- a/test/TestOctalPermissions.py
+++ b/test/TestOctalPermissions.py
@@ -1,33 +1,10 @@
 import unittest
 import ansiblelint.utils
-from itertools import permutations, combinations
+from itertools import product  # Cartesian product: all subsets of length n
 from ansiblelint import RulesCollection
 from ansiblelint.rules.OctalPermissionsRule import OctalPermissionsRule
 
 
-class TestOctalPermissionsRule(unittest.TestCase):
-    rule = OctalPermissionsRule()
-    one_to_seven = [str(digit) for digit in range(8)]
-    # All possible modes are any permutation of three digits in 1-7
-    bad_permutations = set(permutations(combinations(one_to_seven, 3), 3))
-    # Join tuples to strings and flatten the list
-    modes = ["".join(tup) for lst in bad_permutations for tup in lst]
-    bad_modes = ["    mode: " + mode for mode in modes]
-    # Valid modes are just the bad ones with a leading zero
-    good_modes = ["    mode: 0" + mode for mode in modes]
-
-    # Ensure that the given regex matches all octal numbers appropriately
-    def test_regex_positives(self):
-        for good in self.good_modes:
-            self.assertRegexpMatches(good, self.rule.mode_regex)
-            self.assertRegexpMatches(good, self.rule.valid_mode_regex)
-
-    def test_regex_negatives(self):
-        for bad in self.bad_modes:
-            self.assertRegexpMatches(bad, self.rule.mode_regex)
-            self.assertNotRegexpMatches(bad, self.rule.valid_mode_regex)
-
-
 class TestOctalPermissionsRuleWithFile(unittest.TestCase):
     collection = RulesCollection()
 
@@ -37,10 +14,9 @@ def test_file_positive(self):
         good_runner = ansiblelint.Runner(self.collection, [success], [], [], [])
         self.assertEqual([], good_runner.run())
 
-    def test_files(self):
+    def test_file_negative(self):
         self.collection.register(OctalPermissionsRule())
         failure = 'test/octalpermissions-failure.yml'
         bad_runner = ansiblelint.Runner(self.collection, [failure], [], [], [])
         errs = bad_runner.run()
-        # TODO: all errors are counted twice. Why?
-        self.assertEqual(6, len(errs))
+        self.assertEqual(5, len(errs))
diff --git a/test/octalpermissions-failure.yml b/test/octalpermissions-failure.yml
--- a/test/octalpermissions-failure.yml
+++ b/test/octalpermissions-failure.yml
@@ -17,3 +17,13 @@
       file:
         path: foo
         mode: 123
+
+    - name: octal permissions test fail (4000)
+      file:
+        path: baz
+        mode: 4000
+
+    - name: octal permissions test fail (2000)
+      file:
+        path: bar
+        mode: 2000
diff --git a/test/octalpermissions-success.yml b/test/octalpermissions-success.yml
--- a/test/octalpermissions-success.yml
+++ b/test/octalpermissions-success.yml
@@ -17,3 +17,13 @@
       file:
         path: foo
         mode: 0123
+
+    - name: octal permissions test success (04000)
+      file:
+        path: baz
+        mode: 04000
+
+    - name: octal permissions test success (02000)
+      file:
+        path: bar
+        mode: 02000