Repository: ansible/ansible
Pull request: Add SELinux Context as a return value for `module:stat`
##### SUMMARY

<!--- Describe the change below, including rationale and design decisions -->
Add an option `get_selinux_context` to `stat` module, with default value `False` . If it is specified as `True`, the values of `selinux_context` will be added to the return result. resolve #85217
<!--- Add "Fixes #1234" or steps to reproduce the problem if there is no corresponding issue -->

##### ISSUE TYPE

<!--- Pick one below and delete the rest -->

- Feature Pull Request


Problem statement:
stat: Add selinux context as a return value
### Summary

I wanted to use the stat module to check the selinux context of a directory before running restorecon, but I just saw that stat does not output such information currently. I didn't find any other module that could output the selinux context of a file, and using a separate task to register a 'stat -c %C' command seems cumbersome.

Linux stat is able to display such info, and the ansible docs does say that this module aims to be similar to what Linux stat can do...

### Issue Type

Feature Idea

### Component Name

stat

### Additional Information

This is my current stat module output when using core 2.18.6:
```
test | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.9"
    },
    "changed": false,
    "stat": {
        "atime": 1748447418.4292498,
        "attr_flags": "",
        "attributes": [],
        "block_size": 4096,
        "blocks": 0,
        "charset": "binary",
        "ctime": 1748437946.9767725,
        "dev": 64768,
        "device_type": 0,
        "executable": true,
        "exists": true,
        "gid": 23,
        "gr_name": "squid",
        "inode": 340487,
        "isblk": false,
        "ischr": false,
        "isdir": true,
        "isfifo": false,
        "isgid": false,
        "islnk": false,
        "isreg": false,
        "issock": false,
        "isuid": false,
        "mimetype": "inode/directory",
        "mode": "0755",
        "mtime": 1748437939.339103,
        "nlink": 3,
        "path": "/var/lib/ssl_db",
        "pw_name": "squid",
        "readable": true,
        "rgrp": true,
        "roth": true,
        "rusr": true,
        "size": 48,
        "uid": 23,
        "version": "637229437",
        "wgrp": false,
        "woth": false,
        "writeable": false,
        "wusr": true,
        "xgrp": true,
        "xoth": true,
        "xusr": true
    }
}
```

### Code of Conduct

- [x] I agree to follow the Ansible Code of Conduct

Hints from issue discussion:
Files identified in the description:

* [`lib/ansible/modules/stat.py`](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/stat.py)

If these files are incorrect, please update the `component name` section of the description or use the [component](https://github.com/ansible/ansibotmini#commands) bot command.

<!--- boilerplate: components_banner --->

I agree with @ryester19 and SELinux related methods have been implemented in this file `lib/ansible/module_utils/basic.py`, I can open a PR for this. But what I'm worried about is：
1. Current `stat` module only depends on the Python `os.stat()`, if add SELinux Context, which would let dependencies become complicated;
2. Maybe only a few people need SELinux Context.

So, Is it possible to add a `get_selinux_context` option, with the default value being false, and the user specifying true to add queries for related attributes?
`AnsibleModule` is already 'selinux' aware, query `module.selinux_enabled`  or just call `module.selinux_context(path)`, which returns a default/empty context if not enabled
> `AnsibleModule` is already 'selinux' aware, query `module.selinux_enabled` or just call `module.selinux_context(path)`, which returns a default/empty context if not enabled

I will open a PR within couple days.

Commit message: detail description and add negetive test case.

Patch to review:
diff --git a/changelogs/fragments/85217-stat-add-selinux-context.yml b/changelogs/fragments/85217-stat-add-selinux-context.yml
new file mode 100644
index 00000000000000..4879daea65e24e
--- /dev/null
+++ b/changelogs/fragments/85217-stat-add-selinux-context.yml
@@ -0,0 +1,2 @@
+minor_changes:
+  - stat module - add SELinux context as a return value (https://github.com/ansible/ansible/issues/85217).
diff --git a/lib/ansible/modules/stat.py b/lib/ansible/modules/stat.py
index 81707af7a86349..68ced6eeb77b66 100644
--- a/lib/ansible/modules/stat.py
+++ b/lib/ansible/modules/stat.py
@@ -44,6 +44,14 @@
     version_added: "2.3"
   get_checksum:
     version_added: "1.8"
+  get_selinux_context:
+    description:
+      - Get file SELinux context in a list [ user, role, type, range ], 
+      - and will get [None, None, None, None] if it is not possible to retrieve the context, 
+      - either because it does not exist or some other issue.
+    type: bool
+    default: no
+    version_added: '2.19'
 extends_documentation_fragment:
   -  action_common_attributes
   -  checksum_common
@@ -346,6 +354,12 @@
             type: list
             sample: [ immutable, extent ]
             version_added: 2.3
+        selinux_context:
+            description: The SELinux context of a file/directory
+            returned: success, path exists and user can execute the path
+            type: list
+            sample: [ user, role, type, range ]
+            version_added: '2.19'
         version:
             description: The version/generation attribute of a file according to the filesystem
             returned: success, path exists, user can execute the path, lsattr is available and filesystem supports
@@ -435,6 +449,7 @@ def main():
             get_checksum=dict(type='bool', default=True),
             get_mime=dict(type='bool', default=True, aliases=['mime', 'mime_type', 'mime-type']),
             get_attributes=dict(type='bool', default=True, aliases=['attr', 'attributes']),
+            get_selinux_context=dict(type='bool', default=False),
             checksum_algorithm=dict(type='str', default='sha1',
                                     choices=['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'],
                                     aliases=['checksum', 'checksum_algo']),
@@ -449,6 +464,7 @@ def main():
     get_attr = module.params.get('get_attributes')
     get_checksum = module.params.get('get_checksum')
     checksum_algorithm = module.params.get('checksum_algorithm')
+    get_selinux_context = module.params.get('get_selinux_context')
 
     # main stat data
     try:
@@ -517,6 +533,10 @@ def main():
             if x in out:
                 output[x] = out[x]
 
+    # try to get SELinux context
+    if get_selinux_context:
+        output['selinux_context'] = module.selinux_context(b_path)
+
     module.exit_json(changed=False, stat=output)
 
 
diff --git a/test/integration/targets/stat/tasks/main.yml b/test/integration/targets/stat/tasks/main.yml
index 374cb2fd0eb911..a79ca9195e5c06 100644
--- a/test/integration/targets/stat/tasks/main.yml
+++ b/test/integration/targets/stat/tasks/main.yml
@@ -177,3 +177,27 @@
         - "stat_result.changed == false"
         - "stat_result.stat.mimetype == 'text/plain'"
         - "stat_result.stat.charset == 'us-ascii'"
+
+- name: check stat a file with get_selinux_context on
+  stat:
+    path: "{{ remote_tmp_dir }}/foo.txt"
+    get_selinux_context: True
+  register: stat_result
+
+- debug: var=stat_result
+
+- assert:
+    that:
+        - "'selinux_context' in stat_result.stat"
+
+- name: check stat a file with get_selinux_context off
+  stat:
+    path: "{{ remote_tmp_dir }}/foo.txt"
+    get_selinux_context: False
+  register: stat_result
+
+- debug: var=stat_result
+
+- assert:
+    that:
+        - "'selinux_context' not in stat_result.stat"


Code review comments:
diff_hunk: @@ -44,6 +44,14 @@
     version_added: "2.3"
   get_checksum:
     version_added: "1.8"
+  get_selinux_context:
+    description:
+      - Get file SELinux context in a list [ user, role, type, range ], 
+      - and will get [None, None, None, None] if it is not possible to retrieve the context, 
+      - either because it does not exist or some other issue.
path: lib/ansible/modules/stat.py
text: @user1:
dash starts 'new paragraph', either rephrase or just remove it, it will be upto the display layer to do any width management

@author:
I removed unnecessary dash.