| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,298 @@ |
| 0 |
+#!/usr/bin/python |
|
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+# Copyright: (c) 2016, Dag Wieers (@dagwieers) <dag@wieers.com> |
|
| 4 |
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) |
|
| 5 |
+ |
|
| 6 |
+from __future__ import absolute_import, division, print_function |
|
| 7 |
+__metaclass__ = type |
|
| 8 |
+ |
|
| 9 |
+ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
| 10 |
+ 'status': ['preview'], |
|
| 11 |
+ 'supported_by': 'community'} |
|
| 12 |
+ |
|
| 13 |
+DOCUMENTATION = r''' |
|
| 14 |
+--- |
|
| 15 |
+module: sefcontext |
|
| 16 |
+short_description: Manages SELinux file context mapping definitions |
|
| 17 |
+description: |
|
| 18 |
+- Manages SELinux file context mapping definitions. |
|
| 19 |
+- Similar to the C(semanage fcontext) command. |
|
| 20 |
+version_added: '2.2' |
|
| 21 |
+options: |
|
| 22 |
+ target: |
|
| 23 |
+ description: |
|
| 24 |
+ - Target path (expression). |
|
| 25 |
+ type: str |
|
| 26 |
+ required: yes |
|
| 27 |
+ aliases: [ path ] |
|
| 28 |
+ ftype: |
|
| 29 |
+ description: |
|
| 30 |
+ - The file type that should have SELinux contexts applied. |
|
| 31 |
+ - "The following file type options are available:" |
|
| 32 |
+ - C(a) for all files, |
|
| 33 |
+ - C(b) for block devices, |
|
| 34 |
+ - C(c) for character devices, |
|
| 35 |
+ - C(d) for directories, |
|
| 36 |
+ - C(f) for regular files, |
|
| 37 |
+ - C(l) for symbolic links, |
|
| 38 |
+ - C(p) for named pipes, |
|
| 39 |
+ - C(s) for socket files. |
|
| 40 |
+ type: str |
|
| 41 |
+ choices: [ a, b, c, d, f, l, p, s ] |
|
| 42 |
+ default: a |
|
| 43 |
+ setype: |
|
| 44 |
+ description: |
|
| 45 |
+ - SELinux type for the specified target. |
|
| 46 |
+ type: str |
|
| 47 |
+ required: yes |
|
| 48 |
+ seuser: |
|
| 49 |
+ description: |
|
| 50 |
+ - SELinux user for the specified target. |
|
| 51 |
+ type: str |
|
| 52 |
+ selevel: |
|
| 53 |
+ description: |
|
| 54 |
+ - SELinux range for the specified target. |
|
| 55 |
+ type: str |
|
| 56 |
+ aliases: [ serange ] |
|
| 57 |
+ state: |
|
| 58 |
+ description: |
|
| 59 |
+ - Whether the SELinux file context must be C(absent) or C(present). |
|
| 60 |
+ type: str |
|
| 61 |
+ choices: [ absent, present ] |
|
| 62 |
+ default: present |
|
| 63 |
+ reload: |
|
| 64 |
+ description: |
|
| 65 |
+ - Reload SELinux policy after commit. |
|
| 66 |
+ - Note that this does not apply SELinux file contexts to existing files. |
|
| 67 |
+ type: bool |
|
| 68 |
+ default: yes |
|
| 69 |
+ ignore_selinux_state: |
|
| 70 |
+ description: |
|
| 71 |
+ - Useful for scenarios (chrooted environment) that you can't get the real SELinux state. |
|
| 72 |
+ type: bool |
|
| 73 |
+ default: no |
|
| 74 |
+ version_added: '2.8' |
|
| 75 |
+notes: |
|
| 76 |
+- The changes are persistent across reboots. |
|
| 77 |
+- The M(sefcontext) module does not modify existing files to the new |
|
| 78 |
+ SELinux context(s), so it is advisable to first create the SELinux |
|
| 79 |
+ file contexts before creating files, or run C(restorecon) manually |
|
| 80 |
+ for the existing files that require the new SELinux file contexts. |
|
| 81 |
+- Not applying SELinux fcontexts to existing files is a deliberate |
|
| 82 |
+ decision as it would be unclear what reported changes would entail |
|
| 83 |
+ to, and there's no guarantee that applying SELinux fcontext does |
|
| 84 |
+ not pick up other unrelated prior changes. |
|
| 85 |
+requirements: |
|
| 86 |
+- libselinux-python |
|
| 87 |
+- policycoreutils-python |
|
| 88 |
+author: |
|
| 89 |
+- Dag Wieers (@dagwieers) |
|
| 90 |
+''' |
|
| 91 |
+ |
|
| 92 |
+EXAMPLES = r''' |
|
| 93 |
+- name: Allow apache to modify files in /srv/git_repos |
|
| 94 |
+ sefcontext: |
|
| 95 |
+ target: '/srv/git_repos(/.*)?' |
|
| 96 |
+ setype: httpd_git_rw_content_t |
|
| 97 |
+ state: present |
|
| 98 |
+ |
|
| 99 |
+- name: Apply new SELinux file context to filesystem |
|
| 100 |
+ command: restorecon -irv /srv/git_repos |
|
| 101 |
+''' |
|
| 102 |
+ |
|
| 103 |
+RETURN = r''' |
|
| 104 |
+# Default return values |
|
| 105 |
+''' |
|
| 106 |
+ |
|
| 107 |
+import traceback |
|
| 108 |
+ |
|
| 109 |
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib |
|
| 110 |
+from ansible.module_utils._text import to_native |
|
| 111 |
+ |
|
| 112 |
+SELINUX_IMP_ERR = None |
|
| 113 |
+try: |
|
| 114 |
+ import selinux |
|
| 115 |
+ HAVE_SELINUX = True |
|
| 116 |
+except ImportError: |
|
| 117 |
+ SELINUX_IMP_ERR = traceback.format_exc() |
|
| 118 |
+ HAVE_SELINUX = False |
|
| 119 |
+ |
|
| 120 |
+SEOBJECT_IMP_ERR = None |
|
| 121 |
+try: |
|
| 122 |
+ import seobject |
|
| 123 |
+ HAVE_SEOBJECT = True |
|
| 124 |
+except ImportError: |
|
| 125 |
+ SEOBJECT_IMP_ERR = traceback.format_exc() |
|
| 126 |
+ HAVE_SEOBJECT = False |
|
| 127 |
+ |
|
| 128 |
+# Add missing entries (backward compatible) |
|
| 129 |
+if HAVE_SEOBJECT: |
|
| 130 |
+ seobject.file_types.update( |
|
| 131 |
+ a=seobject.SEMANAGE_FCONTEXT_ALL, |
|
| 132 |
+ b=seobject.SEMANAGE_FCONTEXT_BLOCK, |
|
| 133 |
+ c=seobject.SEMANAGE_FCONTEXT_CHAR, |
|
| 134 |
+ d=seobject.SEMANAGE_FCONTEXT_DIR, |
|
| 135 |
+ f=seobject.SEMANAGE_FCONTEXT_REG, |
|
| 136 |
+ l=seobject.SEMANAGE_FCONTEXT_LINK, |
|
| 137 |
+ p=seobject.SEMANAGE_FCONTEXT_PIPE, |
|
| 138 |
+ s=seobject.SEMANAGE_FCONTEXT_SOCK, |
|
| 139 |
+ ) |
|
| 140 |
+ |
|
| 141 |
+# Make backward compatible |
|
| 142 |
+option_to_file_type_str = dict( |
|
| 143 |
+ a='all files', |
|
| 144 |
+ b='block device', |
|
| 145 |
+ c='character device', |
|
| 146 |
+ d='directory', |
|
| 147 |
+ f='regular file', |
|
| 148 |
+ l='symbolic link', |
|
| 149 |
+ p='named pipe', |
|
| 150 |
+ s='socket', |
|
| 151 |
+) |
|
| 152 |
+ |
|
| 153 |
+ |
|
| 154 |
+def get_runtime_status(ignore_selinux_state=False): |
|
| 155 |
+ return True if ignore_selinux_state is True else selinux.is_selinux_enabled() |
|
| 156 |
+ |
|
| 157 |
+ |
|
| 158 |
+def semanage_fcontext_exists(sefcontext, target, ftype): |
|
| 159 |
+ ''' Get the SELinux file context mapping definition from policy. Return None if it does not exist. ''' |
|
| 160 |
+ |
|
| 161 |
+ # Beware that records comprise of a string representation of the file_type |
|
| 162 |
+ record = (target, option_to_file_type_str[ftype]) |
|
| 163 |
+ records = sefcontext.get_all() |
|
| 164 |
+ try: |
|
| 165 |
+ return records[record] |
|
| 166 |
+ except KeyError: |
|
| 167 |
+ return None |
|
| 168 |
+ |
|
| 169 |
+ |
|
| 170 |
+def semanage_fcontext_modify(module, result, target, ftype, setype, do_reload, serange, seuser, sestore=''): |
|
| 171 |
+ ''' Add or modify SELinux file context mapping definition to the policy. ''' |
|
| 172 |
+ |
|
| 173 |
+ changed = False |
|
| 174 |
+ prepared_diff = '' |
|
| 175 |
+ |
|
| 176 |
+ try: |
|
| 177 |
+ sefcontext = seobject.fcontextRecords(sestore) |
|
| 178 |
+ sefcontext.set_reload(do_reload) |
|
| 179 |
+ exists = semanage_fcontext_exists(sefcontext, target, ftype) |
|
| 180 |
+ if exists: |
|
| 181 |
+ # Modify existing entry |
|
| 182 |
+ orig_seuser, orig_serole, orig_setype, orig_serange = exists |
|
| 183 |
+ |
|
| 184 |
+ if seuser is None: |
|
| 185 |
+ seuser = orig_seuser |
|
| 186 |
+ if serange is None: |
|
| 187 |
+ serange = orig_serange |
|
| 188 |
+ |
|
| 189 |
+ if setype != orig_setype or seuser != orig_seuser or serange != orig_serange: |
|
| 190 |
+ if not module.check_mode: |
|
| 191 |
+ sefcontext.modify(target, setype, ftype, serange, seuser) |
|
| 192 |
+ changed = True |
|
| 193 |
+ |
|
| 194 |
+ if module._diff: |
|
| 195 |
+ prepared_diff += '# Change to semanage file context mappings\n' |
|
| 196 |
+ prepared_diff += '-%s %s %s:%s:%s:%s\n' % (target, ftype, orig_seuser, orig_serole, orig_setype, orig_serange) |
|
| 197 |
+ prepared_diff += '+%s %s %s:%s:%s:%s\n' % (target, ftype, seuser, orig_serole, setype, serange) |
|
| 198 |
+ else: |
|
| 199 |
+ # Add missing entry |
|
| 200 |
+ if seuser is None: |
|
| 201 |
+ seuser = 'system_u' |
|
| 202 |
+ if serange is None: |
|
| 203 |
+ serange = 's0' |
|
| 204 |
+ |
|
| 205 |
+ if not module.check_mode: |
|
| 206 |
+ sefcontext.add(target, setype, ftype, serange, seuser) |
|
| 207 |
+ changed = True |
|
| 208 |
+ |
|
| 209 |
+ if module._diff: |
|
| 210 |
+ prepared_diff += '# Addition to semanage file context mappings\n' |
|
| 211 |
+ prepared_diff += '+%s %s %s:%s:%s:%s\n' % (target, ftype, seuser, 'object_r', setype, serange) |
|
| 212 |
+ |
|
| 213 |
+ except Exception as e: |
|
| 214 |
+ module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, to_native(e))) |
|
| 215 |
+ |
|
| 216 |
+ if module._diff and prepared_diff: |
|
| 217 |
+ result['diff'] = dict(prepared=prepared_diff) |
|
| 218 |
+ |
|
| 219 |
+ module.exit_json(changed=changed, seuser=seuser, serange=serange, **result) |
|
| 220 |
+ |
|
| 221 |
+ |
|
| 222 |
+def semanage_fcontext_delete(module, result, target, ftype, do_reload, sestore=''): |
|
| 223 |
+ ''' Delete SELinux file context mapping definition from the policy. ''' |
|
| 224 |
+ |
|
| 225 |
+ changed = False |
|
| 226 |
+ prepared_diff = '' |
|
| 227 |
+ |
|
| 228 |
+ try: |
|
| 229 |
+ sefcontext = seobject.fcontextRecords(sestore) |
|
| 230 |
+ sefcontext.set_reload(do_reload) |
|
| 231 |
+ exists = semanage_fcontext_exists(sefcontext, target, ftype) |
|
| 232 |
+ if exists: |
|
| 233 |
+ # Remove existing entry |
|
| 234 |
+ orig_seuser, orig_serole, orig_setype, orig_serange = exists |
|
| 235 |
+ |
|
| 236 |
+ if not module.check_mode: |
|
| 237 |
+ sefcontext.delete(target, ftype) |
|
| 238 |
+ changed = True |
|
| 239 |
+ |
|
| 240 |
+ if module._diff: |
|
| 241 |
+ prepared_diff += '# Deletion to semanage file context mappings\n' |
|
| 242 |
+ prepared_diff += '-%s %s %s:%s:%s:%s\n' % (target, ftype, exists[0], exists[1], exists[2], exists[3]) |
|
| 243 |
+ |
|
| 244 |
+ except Exception as e: |
|
| 245 |
+ module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, to_native(e))) |
|
| 246 |
+ |
|
| 247 |
+ if module._diff and prepared_diff: |
|
| 248 |
+ result['diff'] = dict(prepared=prepared_diff) |
|
| 249 |
+ |
|
| 250 |
+ module.exit_json(changed=changed, **result) |
|
| 251 |
+ |
|
| 252 |
+ |
|
| 253 |
+def main(): |
|
| 254 |
+ module = AnsibleModule( |
|
| 255 |
+ argument_spec=dict( |
|
| 256 |
+ ignore_selinux_state=dict(type='bool', default=False), |
|
| 257 |
+ target=dict(type='str', required=True, aliases=['path']), |
|
| 258 |
+ ftype=dict(type='str', default='a', choices=option_to_file_type_str.keys()), |
|
| 259 |
+ setype=dict(type='str', required=True), |
|
| 260 |
+ seuser=dict(type='str'), |
|
| 261 |
+ selevel=dict(type='str', aliases=['serange']), |
|
| 262 |
+ state=dict(type='str', default='present', choices=['absent', 'present']), |
|
| 263 |
+ reload=dict(type='bool', default=True), |
|
| 264 |
+ ), |
|
| 265 |
+ supports_check_mode=True, |
|
| 266 |
+ ) |
|
| 267 |
+ if not HAVE_SELINUX: |
|
| 268 |
+ module.fail_json(msg=missing_required_lib("libselinux-python"), exception=SELINUX_IMP_ERR)
|
|
| 269 |
+ |
|
| 270 |
+ if not HAVE_SEOBJECT: |
|
| 271 |
+ module.fail_json(msg=missing_required_lib("policycoreutils-python"), exception=SEOBJECT_IMP_ERR)
|
|
| 272 |
+ |
|
| 273 |
+ ignore_selinux_state = module.params['ignore_selinux_state'] |
|
| 274 |
+ |
|
| 275 |
+ if not get_runtime_status(ignore_selinux_state): |
|
| 276 |
+ module.fail_json(msg="SELinux is disabled on this host.") |
|
| 277 |
+ |
|
| 278 |
+ target = module.params['target'] |
|
| 279 |
+ ftype = module.params['ftype'] |
|
| 280 |
+ setype = module.params['setype'] |
|
| 281 |
+ seuser = module.params['seuser'] |
|
| 282 |
+ serange = module.params['selevel'] |
|
| 283 |
+ state = module.params['state'] |
|
| 284 |
+ do_reload = module.params['reload'] |
|
| 285 |
+ |
|
| 286 |
+ result = dict(target=target, ftype=ftype, setype=setype, state=state) |
|
| 287 |
+ |
|
| 288 |
+ if state == 'present': |
|
| 289 |
+ semanage_fcontext_modify(module, result, target, ftype, setype, do_reload, serange, seuser) |
|
| 290 |
+ elif state == 'absent': |
|
| 291 |
+ semanage_fcontext_delete(module, result, target, ftype, do_reload) |
|
| 292 |
+ else: |
|
| 293 |
+ module.fail_json(msg='Invalid value of argument "state": {0}'.format(state))
|
|
| 294 |
+ |
|
| 295 |
+ |
|
| 296 |
+if __name__ == '__main__': |
|
| 297 |
+ main() |