summaryrefslogtreecommitdiff
path: root/lib/ansible/modules/system/selinux.py
blob: 775c87104b042879f4c2c4efaf19fddc13e3275c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2012, Derek Carter<goozbach@friocorte.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type

ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['stableinterface'],
    'supported_by': 'core'
}

DOCUMENTATION = r'''
---
module: selinux
short_description: Change policy and state of SELinux
description:
  - Configures the SELinux mode and policy.
  - A reboot may be required after usage.
  - Ansible will not issue this reboot but will let you know when it is required.
version_added: "0.7"
options:
  policy:
    description:
      - The name of the SELinux policy to use (e.g. C(targeted)) will be required if state is not C(disabled).
  state:
    description:
      - The SELinux mode.
    required: true
    choices: [ disabled, enforcing, permissive ]
  configfile:
    description:
      - The path to the SELinux configuration file, if non-standard.
    default: /etc/selinux/config
    aliases: [ conf, file ]
requirements: [ libselinux-python ]
author:
- Derek Carter (@goozbach) <goozbach@friocorte.com>
'''

EXAMPLES = r'''
- name: Enable SELinux
  selinux:
    policy: targeted
    state: enforcing

- name: Put SELinux in permissive mode, logging actions that would be blocked.
  selinux:
    policy: targeted
    state: permissive

- name: Disable SELinux
  selinux:
    state: disabled
'''

RETURN = r'''
msg:
    description: Messages that describe changes that were made.
    returned: always
    type: str
    sample: Config SELinux state changed from 'disabled' to 'permissive'
configfile:
    description: Path to SELinux configuration file.
    returned: always
    type: str
    sample: /etc/selinux/config
policy:
    description: Name of the SELinux policy.
    returned: always
    type: str
    sample: targeted
state:
    description: SELinux mode.
    returned: always
    type: str
    sample: enforcing
reboot_required:
    description: Whether or not an reboot is required for the changes to take effect.
    returned: always
    type: bool
    sample: true
'''

import os
import re
import tempfile
import traceback

SELINUX_IMP_ERR = None
try:
    import selinux
    HAS_SELINUX = True
except ImportError:
    SELINUX_IMP_ERR = traceback.format_exc()
    HAS_SELINUX = False

from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.facts.utils import get_file_lines


# getter subroutines
def get_config_state(configfile):
    lines = get_file_lines(configfile, strip=False)

    for line in lines:
        stateline = re.match(r'^SELINUX=.*$', line)
        if stateline:
            return line.split('=')[1].strip()


def get_config_policy(configfile):
    lines = get_file_lines(configfile, strip=False)

    for line in lines:
        stateline = re.match(r'^SELINUXTYPE=.*$', line)
        if stateline:
            return line.split('=')[1].strip()


# setter subroutines
def set_config_state(module, state, configfile):
    # SELINUX=permissive
    # edit config file with state value
    stateline = 'SELINUX=%s' % state
    lines = get_file_lines(configfile, strip=False)

    tmpfd, tmpfile = tempfile.mkstemp()

    with open(tmpfile, "w") as write_file:
        for line in lines:
            write_file.write(re.sub(r'^SELINUX=.*', stateline, line) + '\n')

    module.atomic_move(tmpfile, configfile)


def set_state(module, state):
    if state == 'enforcing':
        selinux.security_setenforce(1)
    elif state == 'permissive':
        selinux.security_setenforce(0)
    elif state == 'disabled':
        pass
    else:
        msg = 'trying to set invalid runtime state %s' % state
        module.fail_json(msg=msg)


def set_config_policy(module, policy, configfile):
    if not os.path.exists('/etc/selinux/%s/policy' % policy):
        module.fail_json(msg='Policy %s does not exist in /etc/selinux/' % policy)

    # edit config file with state value
    # SELINUXTYPE=targeted
    policyline = 'SELINUXTYPE=%s' % policy
    lines = get_file_lines(configfile, strip=False)

    tmpfd, tmpfile = tempfile.mkstemp()

    with open(tmpfile, "w") as write_file:
        for line in lines:
            write_file.write(re.sub(r'^SELINUXTYPE=.*', policyline, line) + '\n')

    module.atomic_move(tmpfile, configfile)


def main():
    module = AnsibleModule(
        argument_spec=dict(
            policy=dict(type='str'),
            state=dict(type='str', required='True', choices=['enforcing', 'permissive', 'disabled']),
            configfile=dict(type='str', default='/etc/selinux/config', aliases=['conf', 'file']),
        ),
        supports_check_mode=True,
    )

    if not HAS_SELINUX:
        module.fail_json(msg=missing_required_lib('libselinux-python'), exception=SELINUX_IMP_ERR)

    # global vars
    changed = False
    msgs = []
    configfile = module.params['configfile']
    policy = module.params['policy']
    state = module.params['state']
    runtime_enabled = selinux.is_selinux_enabled()
    runtime_policy = selinux.selinux_getpolicytype()[1]
    runtime_state = 'disabled'
    reboot_required = False

    if runtime_enabled:
        # enabled means 'enforcing' or 'permissive'
        if selinux.security_getenforce():
            runtime_state = 'enforcing'
        else:
            runtime_state = 'permissive'

    if not os.path.isfile(configfile):
        module.fail_json(msg="Unable to find file {0}".format(configfile),
                         details="Please install SELinux-policy package, "
                                 "if this package is not installed previously.")

    config_policy = get_config_policy(configfile)
    config_state = get_config_state(configfile)

    # check to see if policy is set if state is not 'disabled'
    if state != 'disabled':
        if not policy:
            module.fail_json(msg="Policy is required if state is not 'disabled'")
    else:
        if not policy:
            policy = config_policy

    # check changed values and run changes
    if policy != runtime_policy:
        if module.check_mode:
            module.exit_json(changed=True)
        # cannot change runtime policy
        msgs.append("Running SELinux policy changed from '%s' to '%s'" % (runtime_policy, policy))
        changed = True

    if policy != config_policy:
        if module.check_mode:
            module.exit_json(changed=True)
        set_config_policy(module, policy, configfile)
        msgs.append("SELinux policy configuration in '%s' changed from '%s' to '%s'" % (configfile, config_policy, policy))
        changed = True

    if state != runtime_state:
        if runtime_enabled:
            if state == 'disabled':
                if runtime_state != 'permissive':
                    # Temporarily set state to permissive
                    if not module.check_mode:
                        set_state(module, 'permissive')
                    module.warn("SELinux state temporarily changed from '%s' to 'permissive'. State change will take effect next reboot." % (runtime_state))
                    changed = True
                else:
                    module.warn('SELinux state change will take effect next reboot')
                reboot_required = True
            else:
                if not module.check_mode:
                    set_state(module, state)
                msgs.append("SELinux state changed from '%s' to '%s'" % (runtime_state, state))

                # Only report changes if the file is changed.
                # This prevents the task from reporting changes every time the task is run.
                changed = True
        else:
            module.warn("Reboot is required to set SELinux state to '%s'" % state)
            reboot_required = True

    if state != config_state:
        if not module.check_mode:
            set_config_state(module, state, configfile)
        msgs.append("Config SELinux state changed from '%s' to '%s'" % (config_state, state))
        changed = True

    module.exit_json(changed=changed, msg=', '.join(msgs), configfile=configfile, policy=policy, state=state, reboot_required=reboot_required)


if __name__ == '__main__':
    main()