summaryrefslogtreecommitdiff
path: root/system/seport.py
blob: bbd049c030c1174583c32914bf8923cdfd07ac74 (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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
#!/usr/bin/python

# (c) 2014, Dan Keder <dan.keder@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

ANSIBLE_METADATA = {'status': ['preview'],
                    'supported_by': 'community',
                    'version': '1.0'}

DOCUMENTATION = '''
---
module: seport
short_description: Manages SELinux network port type definitions
description:
     - Manages SELinux network port type definitions.
version_added: "2.0"
options:
  ports:
    description:
      - Ports or port ranges, separated by a comma
    required: true
    default: null
  proto:
    description:
      - Protocol for the specified port.
    required: true
    default: null
    choices: [ 'tcp', 'udp' ]
  setype:
    description:
      - SELinux type for the specified port.
    required: true
    default: null
  state:
    description:
      - Desired boolean value.
    required: true
    default: present
    choices: [ 'present', 'absent' ]
  reload:
    description:
      - Reload SELinux policy after commit.
    required: false
    default: yes
notes:
   - The changes are persistent across reboots
   - Not tested on any debian based system
requirements: [ 'libselinux-python', 'policycoreutils-python' ]
author: Dan Keder
'''

EXAMPLES = '''
# Allow Apache to listen on tcp port 8888
- seport:
    ports: 8888
    proto: tcp
    setype: http_port_t
    state: present

# Allow sshd to listen on tcp port 8991
- seport:
    ports: 8991
    proto: tcp
    setype: ssh_port_t
    state: present

# Allow memcached to listen on tcp ports 10000-10100 and 10112
- seport:
    ports: 10000-10100,10112
    proto: tcp
    setype: memcache_port_t
    state: present
'''

try:
    import selinux
    HAVE_SELINUX=True
except ImportError:
    HAVE_SELINUX=False

try:
    import seobject
    HAVE_SEOBJECT=True
except ImportError:
    HAVE_SEOBJECT=False

from ansible.module_utils.basic import *
from ansible.module_utils.pycompat24 import get_exception


def semanage_port_get_ports(seport, setype, proto):
    """ Get the list of ports that have the specified type definition.

    :param seport: Instance of seobject.portRecords

    :type setype: str
    :param setype: SELinux type.

    :type proto: str
    :param proto: Protocol ('tcp' or 'udp')

    :rtype: list
    :return: List of ports that have the specified SELinux type.
    """
    records = seport.get_all_by_type()
    if (setype, proto) in records:
        return records[(setype, proto)]
    else:
        return []


def semanage_port_get_type(seport, port, proto):
    """ Get the SELinux type of the specified port.

    :param seport: Instance of seobject.portRecords

    :type port: str
    :param port: Port or port range (example: "8080", "8080-9090")

    :type proto: str
    :param proto: Protocol ('tcp' or 'udp')

    :rtype: tuple
    :return: Tuple containing the SELinux type and MLS/MCS level, or None if not found.
    """
    ports = port.split('-', 1)
    if len(ports) == 1:
        ports.extend(ports)
    key = (int(ports[0]), int(ports[1]), proto)

    records = seport.get_all()
    if key in records:
        return records[key]
    else:
        return None


def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', sestore=''):
    """ Add SELinux port type definition to the policy.

    :type module: AnsibleModule
    :param module: Ansible module

    :type ports: list
    :param ports: List of ports and port ranges to add (e.g. ["8080", "8080-9090"])

    :type proto: str
    :param proto: Protocol ('tcp' or 'udp')

    :type setype: str
    :param setype: SELinux type

    :type do_reload: bool
    :param do_reload: Whether to reload SELinux policy after commit

    :type serange: str
    :param serange: SELinux MLS/MCS range (defaults to 's0')

    :type sestore: str
    :param sestore: SELinux store

    :rtype: bool
    :return: True if the policy was changed, otherwise False
    """
    try:
        seport = seobject.portRecords(sestore)
        seport.set_reload(do_reload)
        change = False
        ports_by_type = semanage_port_get_ports(seport, setype, proto)
        for port in ports:
            if port not in ports_by_type:
                change = True
                port_type = semanage_port_get_type(seport, port, proto)
                if port_type is None and not module.check_mode:
                    seport.add(port, proto, serange, setype)
                elif port_type is not None and not module.check_mode:
                    seport.modify(port, proto, serange, setype)

    except ValueError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
    except IOError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
    except KeyError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
    except OSError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
    except RuntimeError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))

    return change


def semanage_port_del(module, ports, proto, setype, do_reload, sestore=''):
    """ Delete SELinux port type definition from the policy.

    :type module: AnsibleModule
    :param module: Ansible module

    :type ports: list
    :param ports: List of ports and port ranges to delete (e.g. ["8080", "8080-9090"])

    :type proto: str
    :param proto: Protocol ('tcp' or 'udp')

    :type setype: str
    :param setype: SELinux type.

    :type do_reload: bool
    :param do_reload: Whether to reload SELinux policy after commit

    :type sestore: str
    :param sestore: SELinux store

    :rtype: bool
    :return: True if the policy was changed, otherwise False
    """
    try:
        seport = seobject.portRecords(sestore)
        seport.set_reload(do_reload)
        change = False
        ports_by_type = semanage_port_get_ports(seport, setype, proto)
        for port in ports:
            if port in ports_by_type:
                change = True
                if not module.check_mode:
                    seport.delete(port, proto)

    except ValueError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
    except IOError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
    except KeyError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
    except OSError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
    except RuntimeError:
        e = get_exception()
        module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))

    return change


def main():
    module = AnsibleModule(
        argument_spec={
                'ports': {
                    'required': True,
                },
                'proto': {
                    'required': True,
                    'choices': ['tcp', 'udp'],
                },
                'setype': {
                    'required': True,
                },
                'state': {
                    'required': True,
                    'choices': ['present', 'absent'],
                },
                'reload': {
                    'required': False,
                    'type': 'bool',
                    'default': 'yes',
                },
            },
        supports_check_mode=True
    )
    if not HAVE_SELINUX:
        module.fail_json(msg="This module requires libselinux-python")

    if not HAVE_SEOBJECT:
        module.fail_json(msg="This module requires policycoreutils-python")

    if not selinux.is_selinux_enabled():
        module.fail_json(msg="SELinux is disabled on this host.")

    ports = [x.strip() for x in str(module.params['ports']).split(',')]
    proto = module.params['proto']
    setype = module.params['setype']
    state = module.params['state']
    do_reload = module.params['reload']

    result = {
        'ports': ports,
        'proto': proto,
        'setype': setype,
        'state': state,
    }

    if state == 'present':
        result['changed'] = semanage_port_add(module, ports, proto, setype, do_reload)
    elif state == 'absent':
        result['changed'] = semanage_port_del(module, ports, proto, setype, do_reload)
    else:
        module.fail_json(msg='Invalid value of argument "state": {0}'.format(state))

    module.exit_json(**result)


if __name__ == '__main__':
    main()