mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-22 12:50:22 -07:00
Initial commit
This commit is contained in:
commit
aebc1b03fd
4861 changed files with 812621 additions and 0 deletions
142
plugins/modules/identity/ipa/ipa_config.py
Normal file
142
plugins/modules/identity/ipa/ipa_config.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2018, Fran Fitzpatrick <francis.x.fitzpatrick@gmail.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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_config
|
||||
author: Fran Fitzpatrick (@fxfitz)
|
||||
short_description: Manage Global FreeIPA Configuration Settings
|
||||
description:
|
||||
- Modify global configuration settings of a FreeIPA Server.
|
||||
options:
|
||||
ipadefaultloginshell:
|
||||
description: Default shell for new users.
|
||||
aliases: ["loginshell"]
|
||||
type: str
|
||||
ipadefaultemaildomain:
|
||||
description: Default e-mail domain for new users.
|
||||
aliases: ["emaildomain"]
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure the default login shell is bash.
|
||||
ipa_config:
|
||||
ipadefaultloginshell: /bin/bash
|
||||
ipa_host: localhost
|
||||
ipa_user: admin
|
||||
ipa_pass: supersecret
|
||||
|
||||
- name: Ensure the default e-mail domain is ansible.com.
|
||||
ipa_config:
|
||||
ipadefaultemaildomain: ansible.com
|
||||
ipa_host: localhost
|
||||
ipa_user: admin
|
||||
ipa_pass: supersecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
config:
|
||||
description: Configuration as returned by IPA API.
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class ConfigIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(ConfigIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def config_show(self):
|
||||
return self._post_json(method='config_show', name=None)
|
||||
|
||||
def config_mod(self, name, item):
|
||||
return self._post_json(method='config_mod', name=name, item=item)
|
||||
|
||||
|
||||
def get_config_dict(ipadefaultloginshell=None, ipadefaultemaildomain=None):
|
||||
config = {}
|
||||
if ipadefaultloginshell is not None:
|
||||
config['ipadefaultloginshell'] = ipadefaultloginshell
|
||||
if ipadefaultemaildomain is not None:
|
||||
config['ipadefaultemaildomain'] = ipadefaultemaildomain
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_config_diff(client, ipa_config, module_config):
|
||||
return client.get_diff(ipa_data=ipa_config, module_data=module_config)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
module_config = get_config_dict(
|
||||
ipadefaultloginshell=module.params.get('ipadefaultloginshell'),
|
||||
ipadefaultemaildomain=module.params.get('ipadefaultemaildomain'),
|
||||
)
|
||||
ipa_config = client.config_show()
|
||||
diff = get_config_diff(client, ipa_config, module_config)
|
||||
|
||||
changed = False
|
||||
new_config = {}
|
||||
for module_key in diff:
|
||||
if module_config.get(module_key) != ipa_config.get(module_key, None):
|
||||
changed = True
|
||||
new_config.update({module_key: module_config.get(module_key)})
|
||||
|
||||
if changed and not module.check_mode:
|
||||
client.config_mod(name=None, item=new_config)
|
||||
|
||||
return changed, client.config_show()
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(
|
||||
ipadefaultloginshell=dict(type='str', aliases=['loginshell']),
|
||||
ipadefaultemaildomain=dict(type='str', aliases=['emaildomain']),
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
client = ConfigIPAClient(
|
||||
module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot']
|
||||
)
|
||||
|
||||
try:
|
||||
client.login(
|
||||
username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass']
|
||||
)
|
||||
changed, user = ensure(module, client)
|
||||
module.exit_json(changed=changed, user=user)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
326
plugins/modules/identity/ipa/ipa_dnsrecord.py
Normal file
326
plugins/modules/identity/ipa/ipa_dnsrecord.py
Normal file
|
@ -0,0 +1,326 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Abhijeet Kasurde (akasurde@redhat.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': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_dnsrecord
|
||||
author: Abhijeet Kasurde (@Akasurde)
|
||||
short_description: Manage FreeIPA DNS records
|
||||
description:
|
||||
- Add, modify and delete an IPA DNS Record using IPA API.
|
||||
options:
|
||||
zone_name:
|
||||
description:
|
||||
- The DNS zone name to which DNS record needs to be managed.
|
||||
required: true
|
||||
type: str
|
||||
record_name:
|
||||
description:
|
||||
- The DNS record name to manage.
|
||||
required: true
|
||||
aliases: ["name"]
|
||||
type: str
|
||||
record_type:
|
||||
description:
|
||||
- The type of DNS record name.
|
||||
- Currently, 'A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'PTR', 'TXT', 'SRV' and 'MX' are supported.
|
||||
- "'A6', 'CNAME', 'DNAME' and 'TXT' are added in version 2.5."
|
||||
- "'SRV' and 'MX' are added in version 2.8."
|
||||
required: false
|
||||
default: 'A'
|
||||
choices: ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'MX', 'PTR', 'SRV', 'TXT']
|
||||
type: str
|
||||
record_value:
|
||||
description:
|
||||
- Manage DNS record name with this value.
|
||||
- In the case of 'A' or 'AAAA' record types, this will be the IP address.
|
||||
- In the case of 'A6' record type, this will be the A6 Record data.
|
||||
- In the case of 'CNAME' record type, this will be the hostname.
|
||||
- In the case of 'DNAME' record type, this will be the DNAME target.
|
||||
- In the case of 'PTR' record type, this will be the hostname.
|
||||
- In the case of 'TXT' record type, this will be a text.
|
||||
- In the case of 'SRV' record type, this will be a service record.
|
||||
- In the case of 'MX' record type, this will be a mail exchanger record.
|
||||
required: true
|
||||
type: str
|
||||
record_ttl:
|
||||
description:
|
||||
- Set the TTL for the record.
|
||||
- Applies only when adding a new or changing the value of record_value.
|
||||
required: false
|
||||
type: int
|
||||
state:
|
||||
description: State to ensure
|
||||
required: false
|
||||
default: present
|
||||
choices: ["absent", "present"]
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure dns record is present
|
||||
ipa_dnsrecord:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: present
|
||||
zone_name: example.com
|
||||
record_name: vm-001
|
||||
record_type: 'AAAA'
|
||||
record_value: '::1'
|
||||
|
||||
- name: Ensure that dns record exists with a TTL
|
||||
ipa_dnsrecord:
|
||||
name: host02
|
||||
zone_name: example.com
|
||||
record_type: 'AAAA'
|
||||
record_value: '::1'
|
||||
record_ttl: 300
|
||||
ipa_host: ipa.example.com
|
||||
ipa_pass: topsecret
|
||||
state: present
|
||||
|
||||
- name: Ensure a PTR record is present
|
||||
ipa_dnsrecord:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: present
|
||||
zone_name: 2.168.192.in-addr.arpa
|
||||
record_name: 5
|
||||
record_type: 'PTR'
|
||||
record_value: 'internal.ipa.example.com'
|
||||
|
||||
- name: Ensure a TXT record is present
|
||||
ipa_dnsrecord:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: present
|
||||
zone_name: example.com
|
||||
record_name: _kerberos
|
||||
record_type: 'TXT'
|
||||
record_value: 'EXAMPLE.COM'
|
||||
|
||||
- name: Ensure an SRV record is present
|
||||
ipa_dnsrecord:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: present
|
||||
zone_name: example.com
|
||||
record_name: _kerberos._udp.example.com
|
||||
record_type: 'SRV'
|
||||
record_value: '10 50 88 ipa.example.com'
|
||||
|
||||
- name: Ensure an MX record is present
|
||||
ipa_dnsrecord:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: present
|
||||
zone_name: example.com
|
||||
record_name: '@'
|
||||
record_type: 'MX'
|
||||
record_value: '1 mailserver.example.com'
|
||||
|
||||
- name: Ensure that dns record is removed
|
||||
ipa_dnsrecord:
|
||||
name: host01
|
||||
zone_name: example.com
|
||||
record_type: 'AAAA'
|
||||
record_value: '::1'
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
dnsrecord:
|
||||
description: DNS record as returned by IPA API.
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class DNSRecordIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(DNSRecordIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def dnsrecord_find(self, zone_name, record_name):
|
||||
if record_name == '@':
|
||||
return self._post_json(method='dnsrecord_show', name=zone_name, item={'idnsname': record_name, 'all': True})
|
||||
else:
|
||||
return self._post_json(method='dnsrecord_find', name=zone_name, item={'idnsname': record_name, 'all': True})
|
||||
|
||||
def dnsrecord_add(self, zone_name=None, record_name=None, details=None):
|
||||
item = dict(idnsname=record_name)
|
||||
if details['record_type'] == 'A':
|
||||
item.update(a_part_ip_address=details['record_value'])
|
||||
elif details['record_type'] == 'AAAA':
|
||||
item.update(aaaa_part_ip_address=details['record_value'])
|
||||
elif details['record_type'] == 'A6':
|
||||
item.update(a6_part_data=details['record_value'])
|
||||
elif details['record_type'] == 'CNAME':
|
||||
item.update(cname_part_hostname=details['record_value'])
|
||||
elif details['record_type'] == 'DNAME':
|
||||
item.update(dname_part_target=details['record_value'])
|
||||
elif details['record_type'] == 'PTR':
|
||||
item.update(ptr_part_hostname=details['record_value'])
|
||||
elif details['record_type'] == 'TXT':
|
||||
item.update(txtrecord=details['record_value'])
|
||||
elif details['record_type'] == 'SRV':
|
||||
item.update(srvrecord=details['record_value'])
|
||||
elif details['record_type'] == 'MX':
|
||||
item.update(mxrecord=details['record_value'])
|
||||
|
||||
if details.get('record_ttl'):
|
||||
item.update(dnsttl=details['record_ttl'])
|
||||
|
||||
return self._post_json(method='dnsrecord_add', name=zone_name, item=item)
|
||||
|
||||
def dnsrecord_mod(self, zone_name=None, record_name=None, details=None):
|
||||
item = get_dnsrecord_dict(details)
|
||||
item.update(idnsname=record_name)
|
||||
if details.get('record_ttl'):
|
||||
item.update(dnsttl=details['record_ttl'])
|
||||
return self._post_json(method='dnsrecord_mod', name=zone_name, item=item)
|
||||
|
||||
def dnsrecord_del(self, zone_name=None, record_name=None, details=None):
|
||||
item = get_dnsrecord_dict(details)
|
||||
item.update(idnsname=record_name)
|
||||
return self._post_json(method='dnsrecord_del', name=zone_name, item=item)
|
||||
|
||||
|
||||
def get_dnsrecord_dict(details=None):
|
||||
module_dnsrecord = dict()
|
||||
if details['record_type'] == 'A' and details['record_value']:
|
||||
module_dnsrecord.update(arecord=details['record_value'])
|
||||
elif details['record_type'] == 'AAAA' and details['record_value']:
|
||||
module_dnsrecord.update(aaaarecord=details['record_value'])
|
||||
elif details['record_type'] == 'A6' and details['record_value']:
|
||||
module_dnsrecord.update(a6record=details['record_value'])
|
||||
elif details['record_type'] == 'CNAME' and details['record_value']:
|
||||
module_dnsrecord.update(cnamerecord=details['record_value'])
|
||||
elif details['record_type'] == 'DNAME' and details['record_value']:
|
||||
module_dnsrecord.update(dnamerecord=details['record_value'])
|
||||
elif details['record_type'] == 'PTR' and details['record_value']:
|
||||
module_dnsrecord.update(ptrrecord=details['record_value'])
|
||||
elif details['record_type'] == 'TXT' and details['record_value']:
|
||||
module_dnsrecord.update(txtrecord=details['record_value'])
|
||||
elif details['record_type'] == 'SRV' and details['record_value']:
|
||||
module_dnsrecord.update(srvrecord=details['record_value'])
|
||||
elif details['record_type'] == 'MX' and details['record_value']:
|
||||
module_dnsrecord.update(mxrecord=details['record_value'])
|
||||
|
||||
if details.get('record_ttl'):
|
||||
module_dnsrecord.update(dnsttl=details['record_ttl'])
|
||||
|
||||
return module_dnsrecord
|
||||
|
||||
|
||||
def get_dnsrecord_diff(client, ipa_dnsrecord, module_dnsrecord):
|
||||
details = get_dnsrecord_dict(module_dnsrecord)
|
||||
return client.get_diff(ipa_data=ipa_dnsrecord, module_data=details)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
zone_name = module.params['zone_name']
|
||||
record_name = module.params['record_name']
|
||||
record_ttl = module.params.get('record_ttl')
|
||||
state = module.params['state']
|
||||
|
||||
ipa_dnsrecord = client.dnsrecord_find(zone_name, record_name)
|
||||
|
||||
module_dnsrecord = dict(
|
||||
record_type=module.params['record_type'],
|
||||
record_value=module.params['record_value'],
|
||||
record_ttl=to_native(record_ttl, nonstring='passthru'),
|
||||
)
|
||||
|
||||
# ttl is not required to change records
|
||||
if module_dnsrecord['record_ttl'] is None:
|
||||
module_dnsrecord.pop('record_ttl')
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not ipa_dnsrecord:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.dnsrecord_add(zone_name=zone_name,
|
||||
record_name=record_name,
|
||||
details=module_dnsrecord)
|
||||
else:
|
||||
diff = get_dnsrecord_diff(client, ipa_dnsrecord, module_dnsrecord)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.dnsrecord_mod(zone_name=zone_name,
|
||||
record_name=record_name,
|
||||
details=module_dnsrecord)
|
||||
else:
|
||||
if ipa_dnsrecord:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.dnsrecord_del(zone_name=zone_name,
|
||||
record_name=record_name,
|
||||
details=module_dnsrecord)
|
||||
|
||||
return changed, client.dnsrecord_find(zone_name, record_name)
|
||||
|
||||
|
||||
def main():
|
||||
record_types = ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'PTR', 'TXT', 'SRV', 'MX']
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(
|
||||
zone_name=dict(type='str', required=True),
|
||||
record_name=dict(type='str', aliases=['name'], required=True),
|
||||
record_type=dict(type='str', default='A', choices=record_types),
|
||||
record_value=dict(type='str', required=True),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
record_ttl=dict(type='int', required=False),
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
client = DNSRecordIPAClient(
|
||||
module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot']
|
||||
)
|
||||
|
||||
try:
|
||||
client.login(
|
||||
username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass']
|
||||
)
|
||||
changed, record = ensure(module, client)
|
||||
module.exit_json(changed=changed, record=record)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
167
plugins/modules/identity/ipa/ipa_dnszone.py
Normal file
167
plugins/modules/identity/ipa/ipa_dnszone.py
Normal file
|
@ -0,0 +1,167 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Fran Fitzpatrick (francis.x.fitzpatrick@gmail.com)
|
||||
# Borrowed heavily from other work by Abhijeet Kasurde (akasurde@redhat.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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_dnszone
|
||||
author: Fran Fitzpatrick (@fxfitz)
|
||||
short_description: Manage FreeIPA DNS Zones
|
||||
description:
|
||||
- Add and delete an IPA DNS Zones using IPA API
|
||||
options:
|
||||
zone_name:
|
||||
description:
|
||||
- The DNS zone name to which needs to be managed.
|
||||
required: true
|
||||
type: str
|
||||
state:
|
||||
description: State to ensure
|
||||
required: false
|
||||
default: present
|
||||
choices: ["absent", "present"]
|
||||
type: str
|
||||
dynamicupdate:
|
||||
description: Apply dynamic update to zone
|
||||
required: false
|
||||
default: "false"
|
||||
choices: ["false", "true"]
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure dns zone is present
|
||||
ipa_dnszone:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: present
|
||||
zone_name: example.com
|
||||
|
||||
- name: Ensure dns zone is present and is dynamic update
|
||||
ipa_dnszone:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: present
|
||||
zone_name: example.com
|
||||
dynamicupdate: true
|
||||
|
||||
- name: Ensure that dns zone is removed
|
||||
ipa_dnszone:
|
||||
zone_name: example.com
|
||||
ipa_host: localhost
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
zone:
|
||||
description: DNS zone as returned by IPA API.
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class DNSZoneIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(DNSZoneIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def dnszone_find(self, zone_name, details=None):
|
||||
itens = {'idnsname': zone_name}
|
||||
if details is not None:
|
||||
itens.update(details)
|
||||
|
||||
return self._post_json(
|
||||
method='dnszone_find',
|
||||
name=zone_name,
|
||||
item=itens
|
||||
)
|
||||
|
||||
def dnszone_add(self, zone_name=None, details=None):
|
||||
itens = {}
|
||||
if details is not None:
|
||||
itens.update(details)
|
||||
|
||||
return self._post_json(
|
||||
method='dnszone_add',
|
||||
name=zone_name,
|
||||
item=itens
|
||||
)
|
||||
|
||||
def dnszone_del(self, zone_name=None, record_name=None, details=None):
|
||||
return self._post_json(
|
||||
method='dnszone_del', name=zone_name, item={})
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
zone_name = module.params['zone_name']
|
||||
state = module.params['state']
|
||||
dynamicupdate = module.params['dynamicupdate']
|
||||
|
||||
ipa_dnszone = client.dnszone_find(zone_name)
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not ipa_dnszone:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.dnszone_add(zone_name=zone_name, details={'idnsallowdynupdate': dynamicupdate})
|
||||
else:
|
||||
changed = False
|
||||
else:
|
||||
if ipa_dnszone:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.dnszone_del(zone_name=zone_name)
|
||||
|
||||
return changed, client.dnszone_find(zone_name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(zone_name=dict(type='str', required=True),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
dynamicupdate=dict(type='str', required=False, default='false', choices=['true', 'false']),
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
client = DNSZoneIPAClient(
|
||||
module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot']
|
||||
)
|
||||
|
||||
try:
|
||||
client.login(
|
||||
username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass']
|
||||
)
|
||||
changed, zone = ensure(module, client)
|
||||
module.exit_json(changed=changed, zone=zone)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
263
plugins/modules/identity/ipa/ipa_group.py
Normal file
263
plugins/modules/identity/ipa/ipa_group.py
Normal file
|
@ -0,0 +1,263 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_group
|
||||
author: Thomas Krahn (@Nosmoht)
|
||||
short_description: Manage FreeIPA group
|
||||
description:
|
||||
- Add, modify and delete group within IPA server
|
||||
options:
|
||||
cn:
|
||||
description:
|
||||
- Canonical name.
|
||||
- Can not be changed as it is the unique identifier.
|
||||
required: true
|
||||
aliases: ['name']
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- Description of the group.
|
||||
type: str
|
||||
external:
|
||||
description:
|
||||
- Allow adding external non-IPA members from trusted domains.
|
||||
type: bool
|
||||
gidnumber:
|
||||
description:
|
||||
- GID (use this option to set it manually).
|
||||
aliases: ['gid']
|
||||
type: str
|
||||
group:
|
||||
description:
|
||||
- List of group names assigned to this group.
|
||||
- If an empty list is passed all groups will be removed from this group.
|
||||
- If option is omitted assigned groups will not be checked or changed.
|
||||
- Groups that are already assigned but not passed will be removed.
|
||||
type: list
|
||||
elements: str
|
||||
nonposix:
|
||||
description:
|
||||
- Create as a non-POSIX group.
|
||||
type: bool
|
||||
user:
|
||||
description:
|
||||
- List of user names assigned to this group.
|
||||
- If an empty list is passed all users will be removed from this group.
|
||||
- If option is omitted assigned users will not be checked or changed.
|
||||
- Users that are already assigned but not passed will be removed.
|
||||
type: list
|
||||
elements: str
|
||||
state:
|
||||
description:
|
||||
- State to ensure
|
||||
default: "present"
|
||||
choices: ["absent", "present"]
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure group is present
|
||||
ipa_group:
|
||||
name: oinstall
|
||||
gidnumber: 54321
|
||||
state: present
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure that groups sysops and appops are assigned to ops but no other group
|
||||
ipa_group:
|
||||
name: ops
|
||||
group:
|
||||
- sysops
|
||||
- appops
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure that users linus and larry are assign to the group, but no other user
|
||||
ipa_group:
|
||||
name: sysops
|
||||
user:
|
||||
- linus
|
||||
- larry
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure group is absent
|
||||
ipa_group:
|
||||
name: sysops
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
group:
|
||||
description: Group as returned by IPA API
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class GroupIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(GroupIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def group_find(self, name):
|
||||
return self._post_json(method='group_find', name=None, item={'all': True, 'cn': name})
|
||||
|
||||
def group_add(self, name, item):
|
||||
return self._post_json(method='group_add', name=name, item=item)
|
||||
|
||||
def group_mod(self, name, item):
|
||||
return self._post_json(method='group_mod', name=name, item=item)
|
||||
|
||||
def group_del(self, name):
|
||||
return self._post_json(method='group_del', name=name)
|
||||
|
||||
def group_add_member(self, name, item):
|
||||
return self._post_json(method='group_add_member', name=name, item=item)
|
||||
|
||||
def group_add_member_group(self, name, item):
|
||||
return self.group_add_member(name=name, item={'group': item})
|
||||
|
||||
def group_add_member_user(self, name, item):
|
||||
return self.group_add_member(name=name, item={'user': item})
|
||||
|
||||
def group_remove_member(self, name, item):
|
||||
return self._post_json(method='group_remove_member', name=name, item=item)
|
||||
|
||||
def group_remove_member_group(self, name, item):
|
||||
return self.group_remove_member(name=name, item={'group': item})
|
||||
|
||||
def group_remove_member_user(self, name, item):
|
||||
return self.group_remove_member(name=name, item={'user': item})
|
||||
|
||||
|
||||
def get_group_dict(description=None, external=None, gid=None, nonposix=None):
|
||||
group = {}
|
||||
if description is not None:
|
||||
group['description'] = description
|
||||
if external is not None:
|
||||
group['external'] = external
|
||||
if gid is not None:
|
||||
group['gidnumber'] = gid
|
||||
if nonposix is not None:
|
||||
group['nonposix'] = nonposix
|
||||
return group
|
||||
|
||||
|
||||
def get_group_diff(client, ipa_group, module_group):
|
||||
data = []
|
||||
# With group_add attribute nonposix is passed, whereas with group_mod only posix can be passed.
|
||||
if 'nonposix' in module_group:
|
||||
# Only non-posix groups can be changed to posix
|
||||
if not module_group['nonposix'] and ipa_group.get('nonposix'):
|
||||
module_group['posix'] = True
|
||||
del module_group['nonposix']
|
||||
|
||||
if 'external' in module_group:
|
||||
if module_group['external'] and 'ipaexternalgroup' in ipa_group.get('objectclass'):
|
||||
del module_group['external']
|
||||
|
||||
return client.get_diff(ipa_data=ipa_group, module_data=module_group)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
state = module.params['state']
|
||||
name = module.params['cn']
|
||||
group = module.params['group']
|
||||
user = module.params['user']
|
||||
|
||||
module_group = get_group_dict(description=module.params['description'], external=module.params['external'],
|
||||
gid=module.params['gidnumber'], nonposix=module.params['nonposix'])
|
||||
ipa_group = client.group_find(name=name)
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not ipa_group:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
ipa_group = client.group_add(name, item=module_group)
|
||||
else:
|
||||
diff = get_group_diff(client, ipa_group, module_group)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
data = {}
|
||||
for key in diff:
|
||||
data[key] = module_group.get(key)
|
||||
client.group_mod(name=name, item=data)
|
||||
|
||||
if group is not None:
|
||||
changed = client.modify_if_diff(name, ipa_group.get('member_group', []), group,
|
||||
client.group_add_member_group,
|
||||
client.group_remove_member_group) or changed
|
||||
|
||||
if user is not None:
|
||||
changed = client.modify_if_diff(name, ipa_group.get('member_user', []), user,
|
||||
client.group_add_member_user,
|
||||
client.group_remove_member_user) or changed
|
||||
|
||||
else:
|
||||
if ipa_group:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.group_del(name)
|
||||
|
||||
return changed, client.group_find(name=name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
|
||||
description=dict(type='str'),
|
||||
external=dict(type='bool'),
|
||||
gidnumber=dict(type='str', aliases=['gid']),
|
||||
group=dict(type='list', elements='str'),
|
||||
nonposix=dict(type='bool'),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
user=dict(type='list', elements='str'))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
client = GroupIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, group = ensure(module, client)
|
||||
module.exit_json(changed=changed, group=group)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
359
plugins/modules/identity/ipa/ipa_hbacrule.py
Normal file
359
plugins/modules/identity/ipa/ipa_hbacrule.py
Normal file
|
@ -0,0 +1,359 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_hbacrule
|
||||
author: Thomas Krahn (@Nosmoht)
|
||||
short_description: Manage FreeIPA HBAC rule
|
||||
description:
|
||||
- Add, modify or delete an IPA HBAC rule using IPA API.
|
||||
options:
|
||||
cn:
|
||||
description:
|
||||
- Canonical name.
|
||||
- Can not be changed as it is the unique identifier.
|
||||
required: true
|
||||
aliases: ["name"]
|
||||
type: str
|
||||
description:
|
||||
description: Description
|
||||
type: str
|
||||
host:
|
||||
description:
|
||||
- List of host names to assign.
|
||||
- If an empty list is passed all hosts will be removed from the rule.
|
||||
- If option is omitted hosts will not be checked or changed.
|
||||
required: false
|
||||
type: list
|
||||
elements: str
|
||||
hostcategory:
|
||||
description: Host category
|
||||
choices: ['all']
|
||||
type: str
|
||||
hostgroup:
|
||||
description:
|
||||
- List of hostgroup names to assign.
|
||||
- If an empty list is passed all hostgroups will be removed. from the rule
|
||||
- If option is omitted hostgroups will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
service:
|
||||
description:
|
||||
- List of service names to assign.
|
||||
- If an empty list is passed all services will be removed from the rule.
|
||||
- If option is omitted services will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
servicecategory:
|
||||
description: Service category
|
||||
choices: ['all']
|
||||
type: str
|
||||
servicegroup:
|
||||
description:
|
||||
- List of service group names to assign.
|
||||
- If an empty list is passed all assigned service groups will be removed from the rule.
|
||||
- If option is omitted service groups will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
sourcehost:
|
||||
description:
|
||||
- List of source host names to assign.
|
||||
- If an empty list if passed all assigned source hosts will be removed from the rule.
|
||||
- If option is omitted source hosts will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
sourcehostcategory:
|
||||
description: Source host category
|
||||
choices: ['all']
|
||||
type: str
|
||||
sourcehostgroup:
|
||||
description:
|
||||
- List of source host group names to assign.
|
||||
- If an empty list if passed all assigned source host groups will be removed from the rule.
|
||||
- If option is omitted source host groups will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
state:
|
||||
description: State to ensure
|
||||
default: "present"
|
||||
choices: ["absent", "disabled", "enabled","present"]
|
||||
type: str
|
||||
user:
|
||||
description:
|
||||
- List of user names to assign.
|
||||
- If an empty list if passed all assigned users will be removed from the rule.
|
||||
- If option is omitted users will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
usercategory:
|
||||
description: User category
|
||||
choices: ['all']
|
||||
type: str
|
||||
usergroup:
|
||||
description:
|
||||
- List of user group names to assign.
|
||||
- If an empty list if passed all assigned user groups will be removed from the rule.
|
||||
- If option is omitted user groups will not be checked or changed.
|
||||
type: list
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure rule to allow all users to access any host from any host
|
||||
ipa_hbacrule:
|
||||
name: allow_all
|
||||
description: Allow all users to access any host from any host
|
||||
hostcategory: all
|
||||
servicecategory: all
|
||||
usercategory: all
|
||||
state: present
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure rule with certain limitations
|
||||
ipa_hbacrule:
|
||||
name: allow_all_developers_access_to_db
|
||||
description: Allow all developers to access any database from any host
|
||||
hostgroup:
|
||||
- db-server
|
||||
usergroup:
|
||||
- developers
|
||||
state: present
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure rule is absent
|
||||
ipa_hbacrule:
|
||||
name: rule_to_be_deleted
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
hbacrule:
|
||||
description: HBAC rule as returned by IPA API.
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class HBACRuleIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(HBACRuleIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def hbacrule_find(self, name):
|
||||
return self._post_json(method='hbacrule_find', name=None, item={'all': True, 'cn': name})
|
||||
|
||||
def hbacrule_add(self, name, item):
|
||||
return self._post_json(method='hbacrule_add', name=name, item=item)
|
||||
|
||||
def hbacrule_mod(self, name, item):
|
||||
return self._post_json(method='hbacrule_mod', name=name, item=item)
|
||||
|
||||
def hbacrule_del(self, name):
|
||||
return self._post_json(method='hbacrule_del', name=name)
|
||||
|
||||
def hbacrule_add_host(self, name, item):
|
||||
return self._post_json(method='hbacrule_add_host', name=name, item=item)
|
||||
|
||||
def hbacrule_remove_host(self, name, item):
|
||||
return self._post_json(method='hbacrule_remove_host', name=name, item=item)
|
||||
|
||||
def hbacrule_add_service(self, name, item):
|
||||
return self._post_json(method='hbacrule_add_service', name=name, item=item)
|
||||
|
||||
def hbacrule_remove_service(self, name, item):
|
||||
return self._post_json(method='hbacrule_remove_service', name=name, item=item)
|
||||
|
||||
def hbacrule_add_user(self, name, item):
|
||||
return self._post_json(method='hbacrule_add_user', name=name, item=item)
|
||||
|
||||
def hbacrule_remove_user(self, name, item):
|
||||
return self._post_json(method='hbacrule_remove_user', name=name, item=item)
|
||||
|
||||
def hbacrule_add_sourcehost(self, name, item):
|
||||
return self._post_json(method='hbacrule_add_sourcehost', name=name, item=item)
|
||||
|
||||
def hbacrule_remove_sourcehost(self, name, item):
|
||||
return self._post_json(method='hbacrule_remove_sourcehost', name=name, item=item)
|
||||
|
||||
|
||||
def get_hbacrule_dict(description=None, hostcategory=None, ipaenabledflag=None, servicecategory=None,
|
||||
sourcehostcategory=None,
|
||||
usercategory=None):
|
||||
data = {}
|
||||
if description is not None:
|
||||
data['description'] = description
|
||||
if hostcategory is not None:
|
||||
data['hostcategory'] = hostcategory
|
||||
if ipaenabledflag is not None:
|
||||
data['ipaenabledflag'] = ipaenabledflag
|
||||
if servicecategory is not None:
|
||||
data['servicecategory'] = servicecategory
|
||||
if sourcehostcategory is not None:
|
||||
data['sourcehostcategory'] = sourcehostcategory
|
||||
if usercategory is not None:
|
||||
data['usercategory'] = usercategory
|
||||
return data
|
||||
|
||||
|
||||
def get_hbcarule_diff(client, ipa_hbcarule, module_hbcarule):
|
||||
return client.get_diff(ipa_data=ipa_hbcarule, module_data=module_hbcarule)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
name = module.params['cn']
|
||||
state = module.params['state']
|
||||
|
||||
if state in ['present', 'enabled']:
|
||||
ipaenabledflag = 'TRUE'
|
||||
else:
|
||||
ipaenabledflag = 'FALSE'
|
||||
|
||||
host = module.params['host']
|
||||
hostcategory = module.params['hostcategory']
|
||||
hostgroup = module.params['hostgroup']
|
||||
service = module.params['service']
|
||||
servicecategory = module.params['servicecategory']
|
||||
servicegroup = module.params['servicegroup']
|
||||
sourcehost = module.params['sourcehost']
|
||||
sourcehostcategory = module.params['sourcehostcategory']
|
||||
sourcehostgroup = module.params['sourcehostgroup']
|
||||
user = module.params['user']
|
||||
usercategory = module.params['usercategory']
|
||||
usergroup = module.params['usergroup']
|
||||
|
||||
module_hbacrule = get_hbacrule_dict(description=module.params['description'],
|
||||
hostcategory=hostcategory,
|
||||
ipaenabledflag=ipaenabledflag,
|
||||
servicecategory=servicecategory,
|
||||
sourcehostcategory=sourcehostcategory,
|
||||
usercategory=usercategory)
|
||||
ipa_hbacrule = client.hbacrule_find(name=name)
|
||||
|
||||
changed = False
|
||||
if state in ['present', 'enabled', 'disabled']:
|
||||
if not ipa_hbacrule:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
ipa_hbacrule = client.hbacrule_add(name=name, item=module_hbacrule)
|
||||
else:
|
||||
diff = get_hbcarule_diff(client, ipa_hbacrule, module_hbacrule)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
data = {}
|
||||
for key in diff:
|
||||
data[key] = module_hbacrule.get(key)
|
||||
client.hbacrule_mod(name=name, item=data)
|
||||
|
||||
if host is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberhost_host', []), host,
|
||||
client.hbacrule_add_host,
|
||||
client.hbacrule_remove_host, 'host') or changed
|
||||
|
||||
if hostgroup is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberhost_hostgroup', []), hostgroup,
|
||||
client.hbacrule_add_host,
|
||||
client.hbacrule_remove_host, 'hostgroup') or changed
|
||||
|
||||
if service is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberservice_hbacsvc', []), service,
|
||||
client.hbacrule_add_service,
|
||||
client.hbacrule_remove_service, 'hbacsvc') or changed
|
||||
|
||||
if servicegroup is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberservice_hbacsvcgroup', []),
|
||||
servicegroup,
|
||||
client.hbacrule_add_service,
|
||||
client.hbacrule_remove_service, 'hbacsvcgroup') or changed
|
||||
|
||||
if sourcehost is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hbacrule.get('sourcehost_host', []), sourcehost,
|
||||
client.hbacrule_add_sourcehost,
|
||||
client.hbacrule_remove_sourcehost, 'host') or changed
|
||||
|
||||
if sourcehostgroup is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hbacrule.get('sourcehost_group', []), sourcehostgroup,
|
||||
client.hbacrule_add_sourcehost,
|
||||
client.hbacrule_remove_sourcehost, 'hostgroup') or changed
|
||||
|
||||
if user is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberuser_user', []), user,
|
||||
client.hbacrule_add_user,
|
||||
client.hbacrule_remove_user, 'user') or changed
|
||||
|
||||
if usergroup is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberuser_group', []), usergroup,
|
||||
client.hbacrule_add_user,
|
||||
client.hbacrule_remove_user, 'group') or changed
|
||||
else:
|
||||
if ipa_hbacrule:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.hbacrule_del(name=name)
|
||||
|
||||
return changed, client.hbacrule_find(name=name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
|
||||
description=dict(type='str'),
|
||||
host=dict(type='list', elements='str'),
|
||||
hostcategory=dict(type='str', choices=['all']),
|
||||
hostgroup=dict(type='list', elements='str'),
|
||||
service=dict(type='list', elements='str'),
|
||||
servicecategory=dict(type='str', choices=['all']),
|
||||
servicegroup=dict(type='list', elements='str'),
|
||||
sourcehost=dict(type='list', elements='str'),
|
||||
sourcehostcategory=dict(type='str', choices=['all']),
|
||||
sourcehostgroup=dict(type='list', elements='str'),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
|
||||
user=dict(type='list', elements='str'),
|
||||
usercategory=dict(type='str', choices=['all']),
|
||||
usergroup=dict(type='list', elements='str'))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
client = HBACRuleIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, hbacrule = ensure(module, client)
|
||||
module.exit_json(changed=changed, hbacrule=hbacrule)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
312
plugins/modules/identity/ipa/ipa_host.py
Normal file
312
plugins/modules/identity/ipa/ipa_host.py
Normal file
|
@ -0,0 +1,312 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_host
|
||||
author: Thomas Krahn (@Nosmoht)
|
||||
short_description: Manage FreeIPA host
|
||||
description:
|
||||
- Add, modify and delete an IPA host using IPA API.
|
||||
options:
|
||||
fqdn:
|
||||
description:
|
||||
- Full qualified domain name.
|
||||
- Can not be changed as it is the unique identifier.
|
||||
required: true
|
||||
aliases: ["name"]
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- A description of this host.
|
||||
type: str
|
||||
force:
|
||||
description:
|
||||
- Force host name even if not in DNS.
|
||||
required: false
|
||||
type: bool
|
||||
ip_address:
|
||||
description:
|
||||
- Add the host to DNS with this IP address.
|
||||
type: str
|
||||
mac_address:
|
||||
description:
|
||||
- List of Hardware MAC address(es) off this host.
|
||||
- If option is omitted MAC addresses will not be checked or changed.
|
||||
- If an empty list is passed all assigned MAC addresses will be removed.
|
||||
- MAC addresses that are already assigned but not passed will be removed.
|
||||
aliases: ["macaddress"]
|
||||
type: list
|
||||
elements: str
|
||||
ns_host_location:
|
||||
description:
|
||||
- Host location (e.g. "Lab 2")
|
||||
aliases: ["nshostlocation"]
|
||||
type: str
|
||||
ns_hardware_platform:
|
||||
description:
|
||||
- Host hardware platform (e.g. "Lenovo T61")
|
||||
aliases: ["nshardwareplatform"]
|
||||
type: str
|
||||
ns_os_version:
|
||||
description:
|
||||
- Host operating system and version (e.g. "Fedora 9")
|
||||
aliases: ["nsosversion"]
|
||||
type: str
|
||||
user_certificate:
|
||||
description:
|
||||
- List of Base-64 encoded server certificates.
|
||||
- If option is omitted certificates will not be checked or changed.
|
||||
- If an empty list is passed all assigned certificates will be removed.
|
||||
- Certificates already assigned but not passed will be removed.
|
||||
aliases: ["usercertificate"]
|
||||
type: list
|
||||
elements: str
|
||||
state:
|
||||
description: State to ensure.
|
||||
default: present
|
||||
choices: ["absent", "disabled", "enabled", "present"]
|
||||
type: str
|
||||
update_dns:
|
||||
description:
|
||||
- If set C("True") with state as C("absent"), then removes DNS records of the host managed by FreeIPA DNS.
|
||||
- This option has no effect for states other than "absent".
|
||||
default: false
|
||||
type: bool
|
||||
random_password:
|
||||
description: Generate a random password to be used in bulk enrollment.
|
||||
default: False
|
||||
type: bool
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure host is present
|
||||
ipa_host:
|
||||
name: host01.example.com
|
||||
description: Example host
|
||||
ip_address: 192.168.0.123
|
||||
ns_host_location: Lab
|
||||
ns_os_version: CentOS 7
|
||||
ns_hardware_platform: Lenovo T61
|
||||
mac_address:
|
||||
- "08:00:27:E3:B1:2D"
|
||||
- "52:54:00:BD:97:1E"
|
||||
state: present
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Generate a random password for bulk enrolment
|
||||
ipa_host:
|
||||
name: host01.example.com
|
||||
description: Example host
|
||||
ip_address: 192.168.0.123
|
||||
state: present
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
validate_certs: False
|
||||
random_password: True
|
||||
|
||||
- name: Ensure host is disabled
|
||||
ipa_host:
|
||||
name: host01.example.com
|
||||
state: disabled
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure that all user certificates are removed
|
||||
ipa_host:
|
||||
name: host01.example.com
|
||||
user_certificate: []
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure host is absent
|
||||
ipa_host:
|
||||
name: host01.example.com
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure host and its DNS record is absent
|
||||
ipa_host:
|
||||
name: host01.example.com
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
update_dns: True
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
host:
|
||||
description: Host as returned by IPA API.
|
||||
returned: always
|
||||
type: dict
|
||||
host_diff:
|
||||
description: List of options that differ and would be changed
|
||||
returned: if check mode and a difference is found
|
||||
type: list
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class HostIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(HostIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def host_show(self, name):
|
||||
return self._post_json(method='host_show', name=name)
|
||||
|
||||
def host_find(self, name):
|
||||
return self._post_json(method='host_find', name=None, item={'all': True, 'fqdn': name})
|
||||
|
||||
def host_add(self, name, host):
|
||||
return self._post_json(method='host_add', name=name, item=host)
|
||||
|
||||
def host_mod(self, name, host):
|
||||
return self._post_json(method='host_mod', name=name, item=host)
|
||||
|
||||
def host_del(self, name, update_dns):
|
||||
return self._post_json(method='host_del', name=name, item={'updatedns': update_dns})
|
||||
|
||||
def host_disable(self, name):
|
||||
return self._post_json(method='host_disable', name=name)
|
||||
|
||||
|
||||
def get_host_dict(description=None, force=None, ip_address=None, ns_host_location=None, ns_hardware_platform=None,
|
||||
ns_os_version=None, user_certificate=None, mac_address=None, random_password=None):
|
||||
data = {}
|
||||
if description is not None:
|
||||
data['description'] = description
|
||||
if force is not None:
|
||||
data['force'] = force
|
||||
if ip_address is not None:
|
||||
data['ip_address'] = ip_address
|
||||
if ns_host_location is not None:
|
||||
data['nshostlocation'] = ns_host_location
|
||||
if ns_hardware_platform is not None:
|
||||
data['nshardwareplatform'] = ns_hardware_platform
|
||||
if ns_os_version is not None:
|
||||
data['nsosversion'] = ns_os_version
|
||||
if user_certificate is not None:
|
||||
data['usercertificate'] = [{"__base64__": item} for item in user_certificate]
|
||||
if mac_address is not None:
|
||||
data['macaddress'] = mac_address
|
||||
if random_password is not None:
|
||||
data['random'] = random_password
|
||||
return data
|
||||
|
||||
|
||||
def get_host_diff(client, ipa_host, module_host):
|
||||
non_updateable_keys = ['force', 'ip_address']
|
||||
if not module_host.get('random'):
|
||||
non_updateable_keys.append('random')
|
||||
for key in non_updateable_keys:
|
||||
if key in module_host:
|
||||
del module_host[key]
|
||||
|
||||
return client.get_diff(ipa_data=ipa_host, module_data=module_host)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
name = module.params['fqdn']
|
||||
state = module.params['state']
|
||||
|
||||
ipa_host = client.host_find(name=name)
|
||||
module_host = get_host_dict(description=module.params['description'],
|
||||
force=module.params['force'], ip_address=module.params['ip_address'],
|
||||
ns_host_location=module.params['ns_host_location'],
|
||||
ns_hardware_platform=module.params['ns_hardware_platform'],
|
||||
ns_os_version=module.params['ns_os_version'],
|
||||
user_certificate=module.params['user_certificate'],
|
||||
mac_address=module.params['mac_address'],
|
||||
random_password=module.params.get('random_password'),
|
||||
)
|
||||
changed = False
|
||||
if state in ['present', 'enabled', 'disabled']:
|
||||
if not ipa_host:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
# OTP password generated by FreeIPA is visible only for host_add command
|
||||
# so, return directly from here.
|
||||
return changed, client.host_add(name=name, host=module_host)
|
||||
else:
|
||||
diff = get_host_diff(client, ipa_host, module_host)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
data = {}
|
||||
for key in diff:
|
||||
data[key] = module_host.get(key)
|
||||
ipa_host_show = client.host_show(name=name)
|
||||
if ipa_host_show.get('has_keytab', False) and module.params.get('random_password'):
|
||||
client.host_disable(name=name)
|
||||
return changed, client.host_mod(name=name, host=data)
|
||||
|
||||
else:
|
||||
if ipa_host:
|
||||
changed = True
|
||||
update_dns = module.params.get('update_dns', False)
|
||||
if not module.check_mode:
|
||||
client.host_del(name=name, update_dns=update_dns)
|
||||
|
||||
return changed, client.host_find(name=name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(description=dict(type='str'),
|
||||
fqdn=dict(type='str', required=True, aliases=['name']),
|
||||
force=dict(type='bool'),
|
||||
ip_address=dict(type='str'),
|
||||
ns_host_location=dict(type='str', aliases=['nshostlocation']),
|
||||
ns_hardware_platform=dict(type='str', aliases=['nshardwareplatform']),
|
||||
ns_os_version=dict(type='str', aliases=['nsosversion']),
|
||||
user_certificate=dict(type='list', aliases=['usercertificate'], elements='str'),
|
||||
mac_address=dict(type='list', aliases=['macaddress'], elements='str'),
|
||||
update_dns=dict(type='bool'),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
|
||||
random_password=dict(type='bool'),)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
client = HostIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, host = ensure(module, client)
|
||||
module.exit_json(changed=changed, host=host)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
213
plugins/modules/identity/ipa/ipa_hostgroup.py
Normal file
213
plugins/modules/identity/ipa/ipa_hostgroup.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_hostgroup
|
||||
author: Thomas Krahn (@Nosmoht)
|
||||
short_description: Manage FreeIPA host-group
|
||||
description:
|
||||
- Add, modify and delete an IPA host-group using IPA API.
|
||||
options:
|
||||
cn:
|
||||
description:
|
||||
- Name of host-group.
|
||||
- Can not be changed as it is the unique identifier.
|
||||
required: true
|
||||
aliases: ["name"]
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- Description.
|
||||
type: str
|
||||
host:
|
||||
description:
|
||||
- List of hosts that belong to the host-group.
|
||||
- If an empty list is passed all hosts will be removed from the group.
|
||||
- If option is omitted hosts will not be checked or changed.
|
||||
- If option is passed all assigned hosts that are not passed will be unassigned from the group.
|
||||
type: list
|
||||
elements: str
|
||||
hostgroup:
|
||||
description:
|
||||
- List of host-groups than belong to that host-group.
|
||||
- If an empty list is passed all host-groups will be removed from the group.
|
||||
- If option is omitted host-groups will not be checked or changed.
|
||||
- If option is passed all assigned hostgroups that are not passed will be unassigned from the group.
|
||||
type: list
|
||||
elements: str
|
||||
state:
|
||||
description:
|
||||
- State to ensure.
|
||||
default: "present"
|
||||
choices: ["absent", "disabled", "enabled", "present"]
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure host-group databases is present
|
||||
ipa_hostgroup:
|
||||
name: databases
|
||||
state: present
|
||||
host:
|
||||
- db.example.com
|
||||
hostgroup:
|
||||
- mysql-server
|
||||
- oracle-server
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure host-group databases is absent
|
||||
ipa_hostgroup:
|
||||
name: databases
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
hostgroup:
|
||||
description: Hostgroup as returned by IPA API.
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class HostGroupIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(HostGroupIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def hostgroup_find(self, name):
|
||||
return self._post_json(method='hostgroup_find', name=None, item={'all': True, 'cn': name})
|
||||
|
||||
def hostgroup_add(self, name, item):
|
||||
return self._post_json(method='hostgroup_add', name=name, item=item)
|
||||
|
||||
def hostgroup_mod(self, name, item):
|
||||
return self._post_json(method='hostgroup_mod', name=name, item=item)
|
||||
|
||||
def hostgroup_del(self, name):
|
||||
return self._post_json(method='hostgroup_del', name=name)
|
||||
|
||||
def hostgroup_add_member(self, name, item):
|
||||
return self._post_json(method='hostgroup_add_member', name=name, item=item)
|
||||
|
||||
def hostgroup_add_host(self, name, item):
|
||||
return self.hostgroup_add_member(name=name, item={'host': item})
|
||||
|
||||
def hostgroup_add_hostgroup(self, name, item):
|
||||
return self.hostgroup_add_member(name=name, item={'hostgroup': item})
|
||||
|
||||
def hostgroup_remove_member(self, name, item):
|
||||
return self._post_json(method='hostgroup_remove_member', name=name, item=item)
|
||||
|
||||
def hostgroup_remove_host(self, name, item):
|
||||
return self.hostgroup_remove_member(name=name, item={'host': item})
|
||||
|
||||
def hostgroup_remove_hostgroup(self, name, item):
|
||||
return self.hostgroup_remove_member(name=name, item={'hostgroup': item})
|
||||
|
||||
|
||||
def get_hostgroup_dict(description=None):
|
||||
data = {}
|
||||
if description is not None:
|
||||
data['description'] = description
|
||||
return data
|
||||
|
||||
|
||||
def get_hostgroup_diff(client, ipa_hostgroup, module_hostgroup):
|
||||
return client.get_diff(ipa_data=ipa_hostgroup, module_data=module_hostgroup)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
name = module.params['cn']
|
||||
state = module.params['state']
|
||||
host = module.params['host']
|
||||
hostgroup = module.params['hostgroup']
|
||||
|
||||
ipa_hostgroup = client.hostgroup_find(name=name)
|
||||
module_hostgroup = get_hostgroup_dict(description=module.params['description'])
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not ipa_hostgroup:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
ipa_hostgroup = client.hostgroup_add(name=name, item=module_hostgroup)
|
||||
else:
|
||||
diff = get_hostgroup_diff(client, ipa_hostgroup, module_hostgroup)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
data = {}
|
||||
for key in diff:
|
||||
data[key] = module_hostgroup.get(key)
|
||||
client.hostgroup_mod(name=name, item=data)
|
||||
|
||||
if host is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hostgroup.get('member_host', []), [item.lower() for item in host],
|
||||
client.hostgroup_add_host, client.hostgroup_remove_host) or changed
|
||||
|
||||
if hostgroup is not None:
|
||||
changed = client.modify_if_diff(name, ipa_hostgroup.get('member_hostgroup', []),
|
||||
[item.lower() for item in hostgroup],
|
||||
client.hostgroup_add_hostgroup,
|
||||
client.hostgroup_remove_hostgroup) or changed
|
||||
|
||||
else:
|
||||
if ipa_hostgroup:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.hostgroup_del(name=name)
|
||||
|
||||
return changed, client.hostgroup_find(name=name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
|
||||
description=dict(type='str'),
|
||||
host=dict(type='list', elements='str'),
|
||||
hostgroup=dict(type='list', elements='str'),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
client = HostGroupIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, hostgroup = ensure(module, client)
|
||||
module.exit_json(changed=changed, hostgroup=hostgroup)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
307
plugins/modules/identity/ipa/ipa_role.py
Normal file
307
plugins/modules/identity/ipa/ipa_role.py
Normal file
|
@ -0,0 +1,307 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_role
|
||||
author: Thomas Krahn (@Nosmoht)
|
||||
short_description: Manage FreeIPA role
|
||||
description:
|
||||
- Add, modify and delete a role within FreeIPA server using FreeIPA API.
|
||||
options:
|
||||
cn:
|
||||
description:
|
||||
- Role name.
|
||||
- Can not be changed as it is the unique identifier.
|
||||
required: true
|
||||
aliases: ['name']
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- A description of this role-group.
|
||||
type: str
|
||||
group:
|
||||
description:
|
||||
- List of group names assign to this role.
|
||||
- If an empty list is passed all assigned groups will be unassigned from the role.
|
||||
- If option is omitted groups will not be checked or changed.
|
||||
- If option is passed all assigned groups that are not passed will be unassigned from the role.
|
||||
type: list
|
||||
elements: str
|
||||
host:
|
||||
description:
|
||||
- List of host names to assign.
|
||||
- If an empty list is passed all assigned hosts will be unassigned from the role.
|
||||
- If option is omitted hosts will not be checked or changed.
|
||||
- If option is passed all assigned hosts that are not passed will be unassigned from the role.
|
||||
type: list
|
||||
elements: str
|
||||
hostgroup:
|
||||
description:
|
||||
- List of host group names to assign.
|
||||
- If an empty list is passed all assigned host groups will be removed from the role.
|
||||
- If option is omitted host groups will not be checked or changed.
|
||||
- If option is passed all assigned hostgroups that are not passed will be unassigned from the role.
|
||||
type: list
|
||||
elements: str
|
||||
privilege:
|
||||
description:
|
||||
- List of privileges granted to the role.
|
||||
- If an empty list is passed all assigned privileges will be removed.
|
||||
- If option is omitted privileges will not be checked or changed.
|
||||
- If option is passed all assigned privileges that are not passed will be removed.
|
||||
type: list
|
||||
elements: str
|
||||
service:
|
||||
description:
|
||||
- List of service names to assign.
|
||||
- If an empty list is passed all assigned services will be removed from the role.
|
||||
- If option is omitted services will not be checked or changed.
|
||||
- If option is passed all assigned services that are not passed will be removed from the role.
|
||||
type: list
|
||||
elements: str
|
||||
state:
|
||||
description: State to ensure.
|
||||
default: "present"
|
||||
choices: ["absent", "present"]
|
||||
type: str
|
||||
user:
|
||||
description:
|
||||
- List of user names to assign.
|
||||
- If an empty list is passed all assigned users will be removed from the role.
|
||||
- If option is omitted users will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure role is present
|
||||
ipa_role:
|
||||
name: dba
|
||||
description: Database Administrators
|
||||
state: present
|
||||
user:
|
||||
- pinky
|
||||
- brain
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure role with certain details
|
||||
ipa_role:
|
||||
name: another-role
|
||||
description: Just another role
|
||||
group:
|
||||
- editors
|
||||
host:
|
||||
- host01.example.com
|
||||
hostgroup:
|
||||
- hostgroup01
|
||||
privilege:
|
||||
- Group Administrators
|
||||
- User Administrators
|
||||
service:
|
||||
- service01
|
||||
|
||||
- name: Ensure role is absent
|
||||
ipa_role:
|
||||
name: dba
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
role:
|
||||
description: Role as returned by IPA API.
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class RoleIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(RoleIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def role_find(self, name):
|
||||
return self._post_json(method='role_find', name=None, item={'all': True, 'cn': name})
|
||||
|
||||
def role_add(self, name, item):
|
||||
return self._post_json(method='role_add', name=name, item=item)
|
||||
|
||||
def role_mod(self, name, item):
|
||||
return self._post_json(method='role_mod', name=name, item=item)
|
||||
|
||||
def role_del(self, name):
|
||||
return self._post_json(method='role_del', name=name)
|
||||
|
||||
def role_add_member(self, name, item):
|
||||
return self._post_json(method='role_add_member', name=name, item=item)
|
||||
|
||||
def role_add_group(self, name, item):
|
||||
return self.role_add_member(name=name, item={'group': item})
|
||||
|
||||
def role_add_host(self, name, item):
|
||||
return self.role_add_member(name=name, item={'host': item})
|
||||
|
||||
def role_add_hostgroup(self, name, item):
|
||||
return self.role_add_member(name=name, item={'hostgroup': item})
|
||||
|
||||
def role_add_service(self, name, item):
|
||||
return self.role_add_member(name=name, item={'service': item})
|
||||
|
||||
def role_add_user(self, name, item):
|
||||
return self.role_add_member(name=name, item={'user': item})
|
||||
|
||||
def role_remove_member(self, name, item):
|
||||
return self._post_json(method='role_remove_member', name=name, item=item)
|
||||
|
||||
def role_remove_group(self, name, item):
|
||||
return self.role_remove_member(name=name, item={'group': item})
|
||||
|
||||
def role_remove_host(self, name, item):
|
||||
return self.role_remove_member(name=name, item={'host': item})
|
||||
|
||||
def role_remove_hostgroup(self, name, item):
|
||||
return self.role_remove_member(name=name, item={'hostgroup': item})
|
||||
|
||||
def role_remove_service(self, name, item):
|
||||
return self.role_remove_member(name=name, item={'service': item})
|
||||
|
||||
def role_remove_user(self, name, item):
|
||||
return self.role_remove_member(name=name, item={'user': item})
|
||||
|
||||
def role_add_privilege(self, name, item):
|
||||
return self._post_json(method='role_add_privilege', name=name, item={'privilege': item})
|
||||
|
||||
def role_remove_privilege(self, name, item):
|
||||
return self._post_json(method='role_remove_privilege', name=name, item={'privilege': item})
|
||||
|
||||
|
||||
def get_role_dict(description=None):
|
||||
data = {}
|
||||
if description is not None:
|
||||
data['description'] = description
|
||||
return data
|
||||
|
||||
|
||||
def get_role_diff(client, ipa_role, module_role):
|
||||
return client.get_diff(ipa_data=ipa_role, module_data=module_role)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
state = module.params['state']
|
||||
name = module.params['cn']
|
||||
group = module.params['group']
|
||||
host = module.params['host']
|
||||
hostgroup = module.params['hostgroup']
|
||||
privilege = module.params['privilege']
|
||||
service = module.params['service']
|
||||
user = module.params['user']
|
||||
|
||||
module_role = get_role_dict(description=module.params['description'])
|
||||
ipa_role = client.role_find(name=name)
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not ipa_role:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
ipa_role = client.role_add(name=name, item=module_role)
|
||||
else:
|
||||
diff = get_role_diff(client, ipa_role, module_role)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
data = {}
|
||||
for key in diff:
|
||||
data[key] = module_role.get(key)
|
||||
client.role_mod(name=name, item=data)
|
||||
|
||||
if group is not None:
|
||||
changed = client.modify_if_diff(name, ipa_role.get('member_group', []), group,
|
||||
client.role_add_group,
|
||||
client.role_remove_group) or changed
|
||||
if host is not None:
|
||||
changed = client.modify_if_diff(name, ipa_role.get('member_host', []), host,
|
||||
client.role_add_host,
|
||||
client.role_remove_host) or changed
|
||||
|
||||
if hostgroup is not None:
|
||||
changed = client.modify_if_diff(name, ipa_role.get('member_hostgroup', []), hostgroup,
|
||||
client.role_add_hostgroup,
|
||||
client.role_remove_hostgroup) or changed
|
||||
|
||||
if privilege is not None:
|
||||
changed = client.modify_if_diff(name, ipa_role.get('memberof_privilege', []), privilege,
|
||||
client.role_add_privilege,
|
||||
client.role_remove_privilege) or changed
|
||||
if service is not None:
|
||||
changed = client.modify_if_diff(name, ipa_role.get('member_service', []), service,
|
||||
client.role_add_service,
|
||||
client.role_remove_service) or changed
|
||||
if user is not None:
|
||||
changed = client.modify_if_diff(name, ipa_role.get('member_user', []), user,
|
||||
client.role_add_user,
|
||||
client.role_remove_user) or changed
|
||||
|
||||
else:
|
||||
if ipa_role:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.role_del(name)
|
||||
|
||||
return changed, client.role_find(name=name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
|
||||
description=dict(type='str'),
|
||||
group=dict(type='list', elements='str'),
|
||||
host=dict(type='list', elements='str'),
|
||||
hostgroup=dict(type='list', elements='str'),
|
||||
privilege=dict(type='list', elements='str'),
|
||||
service=dict(type='list', elements='str'),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
user=dict(type='list', elements='str'))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
client = RoleIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, role = ensure(module, client)
|
||||
module.exit_json(changed=changed, role=role)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
213
plugins/modules/identity/ipa/ipa_service.py
Normal file
213
plugins/modules/identity/ipa/ipa_service.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2018, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_service
|
||||
author: Cédric Parent (@cprh)
|
||||
short_description: Manage FreeIPA service
|
||||
description:
|
||||
- Add and delete an IPA service using IPA API.
|
||||
options:
|
||||
krbcanonicalname:
|
||||
description:
|
||||
- Principal of the service.
|
||||
- Can not be changed as it is the unique identifier.
|
||||
required: true
|
||||
aliases: ["name"]
|
||||
type: str
|
||||
hosts:
|
||||
description:
|
||||
- Defines the list of 'ManagedBy' hosts.
|
||||
required: false
|
||||
type: list
|
||||
elements: str
|
||||
force:
|
||||
description:
|
||||
- Force principal name even if host is not in DNS.
|
||||
required: false
|
||||
type: bool
|
||||
state:
|
||||
description: State to ensure.
|
||||
required: false
|
||||
default: present
|
||||
choices: ["absent", "present"]
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure service is present
|
||||
ipa_service:
|
||||
name: http/host01.example.com
|
||||
state: present
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure service is absent
|
||||
ipa_service:
|
||||
name: http/host01.example.com
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Changing Managing hosts list
|
||||
ipa_service:
|
||||
name: http/host01.example.com
|
||||
host:
|
||||
- host01.example.com
|
||||
- host02.example.com
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
service:
|
||||
description: Service as returned by IPA API.
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class ServiceIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(ServiceIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def service_find(self, name):
|
||||
return self._post_json(method='service_find', name=None, item={'all': True, 'krbcanonicalname': name})
|
||||
|
||||
def service_add(self, name, service):
|
||||
return self._post_json(method='service_add', name=name, item=service)
|
||||
|
||||
def service_mod(self, name, service):
|
||||
return self._post_json(method='service_mod', name=name, item=service)
|
||||
|
||||
def service_del(self, name):
|
||||
return self._post_json(method='service_del', name=name)
|
||||
|
||||
def service_disable(self, name):
|
||||
return self._post_json(method='service_disable', name=name)
|
||||
|
||||
def service_add_host(self, name, item):
|
||||
return self._post_json(method='service_add_host', name=name, item={'host': item})
|
||||
|
||||
def service_remove_host(self, name, item):
|
||||
return self._post_json(method='service_remove_host', name=name, item={'host': item})
|
||||
|
||||
|
||||
def get_service_dict(force=None, krbcanonicalname=None):
|
||||
data = {}
|
||||
if force is not None:
|
||||
data['force'] = force
|
||||
if krbcanonicalname is not None:
|
||||
data['krbcanonicalname'] = krbcanonicalname
|
||||
return data
|
||||
|
||||
|
||||
def get_service_diff(client, ipa_host, module_service):
|
||||
non_updateable_keys = ['force', 'krbcanonicalname']
|
||||
for key in non_updateable_keys:
|
||||
if key in module_service:
|
||||
del module_service[key]
|
||||
|
||||
return client.get_diff(ipa_data=ipa_host, module_data=module_service)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
name = module.params['krbcanonicalname']
|
||||
state = module.params['state']
|
||||
hosts = module.params['hosts']
|
||||
|
||||
ipa_service = client.service_find(name=name)
|
||||
module_service = get_service_dict(force=module.params['force'])
|
||||
changed = False
|
||||
if state in ['present', 'enabled', 'disabled']:
|
||||
if not ipa_service:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.service_add(name=name, service=module_service)
|
||||
else:
|
||||
diff = get_service_diff(client, ipa_service, module_service)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
data = {}
|
||||
for key in diff:
|
||||
data[key] = module_service.get(key)
|
||||
client.service_mod(name=name, service=data)
|
||||
if hosts is not None:
|
||||
if 'managedby_host' in ipa_service:
|
||||
for host in ipa_service['managedby_host']:
|
||||
if host not in hosts:
|
||||
if not module.check_mode:
|
||||
client.service_remove_host(name=name, item=host)
|
||||
changed = True
|
||||
for host in hosts:
|
||||
if host not in ipa_service['managedby_host']:
|
||||
if not module.check_mode:
|
||||
client.service_add_host(name=name, item=host)
|
||||
changed = True
|
||||
else:
|
||||
for host in hosts:
|
||||
if not module.check_mode:
|
||||
client.service_add_host(name=name, item=host)
|
||||
changed = True
|
||||
|
||||
else:
|
||||
if ipa_service:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.service_del(name=name)
|
||||
|
||||
return changed, client.service_find(name=name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(
|
||||
krbcanonicalname=dict(type='str', required=True, aliases=['name']),
|
||||
force=dict(type='bool', required=False),
|
||||
hosts=dict(type='list', required=False, elements='str'),
|
||||
state=dict(type='str', required=False, default='present',
|
||||
choices=['present', 'absent']))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
client = ServiceIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, host = ensure(module, client)
|
||||
module.exit_json(changed=changed, host=host)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
216
plugins/modules/identity/ipa/ipa_subca.py
Normal file
216
plugins/modules/identity/ipa/ipa_subca.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Abhijeet Kasurde (akasurde@redhat.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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_subca
|
||||
author: Abhijeet Kasurde (@Akasurde)
|
||||
short_description: Manage FreeIPA Lightweight Sub Certificate Authorities.
|
||||
description:
|
||||
- Add, modify, enable, disable and delete an IPA Lightweight Sub Certificate Authorities using IPA API.
|
||||
options:
|
||||
subca_name:
|
||||
description:
|
||||
- The Sub Certificate Authority name which needs to be managed.
|
||||
required: true
|
||||
aliases: ["name"]
|
||||
type: str
|
||||
subca_subject:
|
||||
description:
|
||||
- The Sub Certificate Authority's Subject. e.g., 'CN=SampleSubCA1,O=testrelm.test'.
|
||||
required: true
|
||||
type: str
|
||||
subca_desc:
|
||||
description:
|
||||
- The Sub Certificate Authority's description.
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- State to ensure.
|
||||
- State 'disable' and 'enable' is available for FreeIPA 4.4.2 version and onwards.
|
||||
required: false
|
||||
default: present
|
||||
choices: ["absent", "disabled", "enabled", "present"]
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Ensure IPA Sub CA is present
|
||||
ipa_subca:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: present
|
||||
subca_name: AnsibleSubCA1
|
||||
subca_subject: 'CN=AnsibleSubCA1,O=example.com'
|
||||
subca_desc: Ansible Sub CA
|
||||
|
||||
- name: Ensure that IPA Sub CA is removed
|
||||
ipa_subca:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: absent
|
||||
subca_name: AnsibleSubCA1
|
||||
|
||||
- name: Ensure that IPA Sub CA is disabled
|
||||
ipa_subca:
|
||||
ipa_host: spider.example.com
|
||||
ipa_pass: Passw0rd!
|
||||
state: disable
|
||||
subca_name: AnsibleSubCA1
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
subca:
|
||||
description: IPA Sub CA record as returned by IPA API.
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class SubCAIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(SubCAIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def subca_find(self, subca_name):
|
||||
return self._post_json(method='ca_find', name=subca_name, item=None)
|
||||
|
||||
def subca_add(self, subca_name=None, subject_dn=None, details=None):
|
||||
item = dict(ipacasubjectdn=subject_dn)
|
||||
subca_desc = details.get('description', None)
|
||||
if subca_desc is not None:
|
||||
item.update(description=subca_desc)
|
||||
return self._post_json(method='ca_add', name=subca_name, item=item)
|
||||
|
||||
def subca_mod(self, subca_name=None, diff=None, details=None):
|
||||
item = get_subca_dict(details)
|
||||
for change in diff:
|
||||
update_detail = dict()
|
||||
if item[change] is not None:
|
||||
update_detail.update(setattr="{0}={1}".format(change, item[change]))
|
||||
self._post_json(method='ca_mod', name=subca_name, item=update_detail)
|
||||
|
||||
def subca_del(self, subca_name=None):
|
||||
return self._post_json(method='ca_del', name=subca_name)
|
||||
|
||||
def subca_disable(self, subca_name=None):
|
||||
return self._post_json(method='ca_disable', name=subca_name)
|
||||
|
||||
def subca_enable(self, subca_name=None):
|
||||
return self._post_json(method='ca_enable', name=subca_name)
|
||||
|
||||
|
||||
def get_subca_dict(details=None):
|
||||
module_subca = dict()
|
||||
if details['description'] is not None:
|
||||
module_subca['description'] = details['description']
|
||||
if details['subca_subject'] is not None:
|
||||
module_subca['ipacasubjectdn'] = details['subca_subject']
|
||||
return module_subca
|
||||
|
||||
|
||||
def get_subca_diff(client, ipa_subca, module_subca):
|
||||
details = get_subca_dict(module_subca)
|
||||
return client.get_diff(ipa_data=ipa_subca, module_data=details)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
subca_name = module.params['subca_name']
|
||||
subca_subject_dn = module.params['subca_subject']
|
||||
subca_desc = module.params['subca_desc']
|
||||
|
||||
state = module.params['state']
|
||||
|
||||
ipa_subca = client.subca_find(subca_name)
|
||||
module_subca = dict(description=subca_desc,
|
||||
subca_subject=subca_subject_dn)
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not ipa_subca:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.subca_add(subca_name=subca_name, subject_dn=subca_subject_dn, details=module_subca)
|
||||
else:
|
||||
diff = get_subca_diff(client, ipa_subca, module_subca)
|
||||
# IPA does not allow to modify Sub CA's subject DN
|
||||
# So skip it for now.
|
||||
if 'ipacasubjectdn' in diff:
|
||||
diff.remove('ipacasubjectdn')
|
||||
del module_subca['subca_subject']
|
||||
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.subca_mod(subca_name=subca_name, diff=diff, details=module_subca)
|
||||
elif state == 'absent':
|
||||
if ipa_subca:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.subca_del(subca_name=subca_name)
|
||||
elif state == 'disable':
|
||||
ipa_version = client.get_ipa_version()
|
||||
if LooseVersion(ipa_version) < LooseVersion('4.4.2'):
|
||||
module.fail_json(msg="Current version of IPA server [%s] does not support 'CA disable' option. Please upgrade to "
|
||||
"version greater than 4.4.2")
|
||||
if ipa_subca:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.subca_disable(subca_name=subca_name)
|
||||
elif state == 'enable':
|
||||
ipa_version = client.get_ipa_version()
|
||||
if LooseVersion(ipa_version) < LooseVersion('4.4.2'):
|
||||
module.fail_json(msg="Current version of IPA server [%s] does not support 'CA enable' option. Please upgrade to "
|
||||
"version greater than 4.4.2")
|
||||
if ipa_subca:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.subca_enable(subca_name=subca_name)
|
||||
|
||||
return changed, client.subca_find(subca_name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(subca_name=dict(type='str', required=True, aliases=['name']),
|
||||
subca_subject=dict(type='str', required=True),
|
||||
subca_desc=dict(type='str'),
|
||||
state=dict(type='str', default='present',
|
||||
choices=['present', 'absent', 'enabled', 'disabled']),)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True,)
|
||||
|
||||
client = SubCAIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, record = ensure(module, client)
|
||||
module.exit_json(changed=changed, record=record)
|
||||
except Exception as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
156
plugins/modules/identity/ipa/ipa_sudocmd.py
Normal file
156
plugins/modules/identity/ipa/ipa_sudocmd.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_sudocmd
|
||||
author: Thomas Krahn (@Nosmoht)
|
||||
short_description: Manage FreeIPA sudo command
|
||||
description:
|
||||
- Add, modify or delete sudo command within FreeIPA server using FreeIPA API.
|
||||
options:
|
||||
sudocmd:
|
||||
description:
|
||||
- Sudo command.
|
||||
aliases: ['name']
|
||||
required: true
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- A description of this command.
|
||||
type: str
|
||||
state:
|
||||
description: State to ensure.
|
||||
default: present
|
||||
choices: ['absent', 'disabled', 'enabled', 'present']
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure sudo command exists
|
||||
ipa_sudocmd:
|
||||
name: su
|
||||
description: Allow to run su via sudo
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure sudo command does not exist
|
||||
ipa_sudocmd:
|
||||
name: su
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
sudocmd:
|
||||
description: Sudo command as return from IPA API
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class SudoCmdIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(SudoCmdIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def sudocmd_find(self, name):
|
||||
return self._post_json(method='sudocmd_find', name=None, item={'all': True, 'sudocmd': name})
|
||||
|
||||
def sudocmd_add(self, name, item):
|
||||
return self._post_json(method='sudocmd_add', name=name, item=item)
|
||||
|
||||
def sudocmd_mod(self, name, item):
|
||||
return self._post_json(method='sudocmd_mod', name=name, item=item)
|
||||
|
||||
def sudocmd_del(self, name):
|
||||
return self._post_json(method='sudocmd_del', name=name)
|
||||
|
||||
|
||||
def get_sudocmd_dict(description=None):
|
||||
data = {}
|
||||
if description is not None:
|
||||
data['description'] = description
|
||||
return data
|
||||
|
||||
|
||||
def get_sudocmd_diff(client, ipa_sudocmd, module_sudocmd):
|
||||
return client.get_diff(ipa_data=ipa_sudocmd, module_data=module_sudocmd)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
name = module.params['sudocmd']
|
||||
state = module.params['state']
|
||||
|
||||
module_sudocmd = get_sudocmd_dict(description=module.params['description'])
|
||||
ipa_sudocmd = client.sudocmd_find(name=name)
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not ipa_sudocmd:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.sudocmd_add(name=name, item=module_sudocmd)
|
||||
else:
|
||||
diff = get_sudocmd_diff(client, ipa_sudocmd, module_sudocmd)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
data = {}
|
||||
for key in diff:
|
||||
data[key] = module_sudocmd.get(key)
|
||||
client.sudocmd_mod(name=name, item=data)
|
||||
else:
|
||||
if ipa_sudocmd:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.sudocmd_del(name=name)
|
||||
|
||||
return changed, client.sudocmd_find(name=name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(description=dict(type='str'),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
|
||||
sudocmd=dict(type='str', required=True, aliases=['name']))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
client = SudoCmdIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, sudocmd = ensure(module, client)
|
||||
module.exit_json(changed=changed, sudocmd=sudocmd)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
184
plugins/modules/identity/ipa/ipa_sudocmdgroup.py
Normal file
184
plugins/modules/identity/ipa/ipa_sudocmdgroup.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_sudocmdgroup
|
||||
author: Thomas Krahn (@Nosmoht)
|
||||
short_description: Manage FreeIPA sudo command group
|
||||
description:
|
||||
- Add, modify or delete sudo command group within IPA server using IPA API.
|
||||
options:
|
||||
cn:
|
||||
description:
|
||||
- Sudo Command Group.
|
||||
aliases: ['name']
|
||||
required: true
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- Group description.
|
||||
type: str
|
||||
state:
|
||||
description: State to ensure.
|
||||
default: present
|
||||
choices: ['absent', 'disabled', 'enabled', 'present']
|
||||
type: str
|
||||
sudocmd:
|
||||
description:
|
||||
- List of sudo commands to assign to the group.
|
||||
- If an empty list is passed all assigned commands will be removed from the group.
|
||||
- If option is omitted sudo commands will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure sudo command group exists
|
||||
ipa_sudocmdgroup:
|
||||
name: group01
|
||||
description: Group of important commands
|
||||
sudocmd:
|
||||
- su
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure sudo command group does not exist
|
||||
ipa_sudocmdgroup:
|
||||
name: group01
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
sudocmdgroup:
|
||||
description: Sudo command group as returned by IPA API
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class SudoCmdGroupIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(SudoCmdGroupIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def sudocmdgroup_find(self, name):
|
||||
return self._post_json(method='sudocmdgroup_find', name=None, item={'all': True, 'cn': name})
|
||||
|
||||
def sudocmdgroup_add(self, name, item):
|
||||
return self._post_json(method='sudocmdgroup_add', name=name, item=item)
|
||||
|
||||
def sudocmdgroup_mod(self, name, item):
|
||||
return self._post_json(method='sudocmdgroup_mod', name=name, item=item)
|
||||
|
||||
def sudocmdgroup_del(self, name):
|
||||
return self._post_json(method='sudocmdgroup_del', name=name)
|
||||
|
||||
def sudocmdgroup_add_member(self, name, item):
|
||||
return self._post_json(method='sudocmdgroup_add_member', name=name, item=item)
|
||||
|
||||
def sudocmdgroup_add_member_sudocmd(self, name, item):
|
||||
return self.sudocmdgroup_add_member(name=name, item={'sudocmd': item})
|
||||
|
||||
def sudocmdgroup_remove_member(self, name, item):
|
||||
return self._post_json(method='sudocmdgroup_remove_member', name=name, item=item)
|
||||
|
||||
def sudocmdgroup_remove_member_sudocmd(self, name, item):
|
||||
return self.sudocmdgroup_remove_member(name=name, item={'sudocmd': item})
|
||||
|
||||
|
||||
def get_sudocmdgroup_dict(description=None):
|
||||
data = {}
|
||||
if description is not None:
|
||||
data['description'] = description
|
||||
return data
|
||||
|
||||
|
||||
def get_sudocmdgroup_diff(client, ipa_sudocmdgroup, module_sudocmdgroup):
|
||||
return client.get_diff(ipa_data=ipa_sudocmdgroup, module_data=module_sudocmdgroup)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
name = module.params['cn']
|
||||
state = module.params['state']
|
||||
sudocmd = module.params['sudocmd']
|
||||
|
||||
module_sudocmdgroup = get_sudocmdgroup_dict(description=module.params['description'])
|
||||
ipa_sudocmdgroup = client.sudocmdgroup_find(name=name)
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not ipa_sudocmdgroup:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
ipa_sudocmdgroup = client.sudocmdgroup_add(name=name, item=module_sudocmdgroup)
|
||||
else:
|
||||
diff = get_sudocmdgroup_diff(client, ipa_sudocmdgroup, module_sudocmdgroup)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
data = {}
|
||||
for key in diff:
|
||||
data[key] = module_sudocmdgroup.get(key)
|
||||
client.sudocmdgroup_mod(name=name, item=data)
|
||||
|
||||
if sudocmd is not None:
|
||||
changed = client.modify_if_diff(name, ipa_sudocmdgroup.get('member_sudocmd', []), sudocmd,
|
||||
client.sudocmdgroup_add_member_sudocmd,
|
||||
client.sudocmdgroup_remove_member_sudocmd)
|
||||
else:
|
||||
if ipa_sudocmdgroup:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.sudocmdgroup_del(name=name)
|
||||
|
||||
return changed, client.sudocmdgroup_find(name=name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
|
||||
description=dict(type='str'),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
|
||||
sudocmd=dict(type='list', elements='str'))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
client = SudoCmdGroupIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, sudocmdgroup = ensure(module, client)
|
||||
module.exit_json(changed=changed, sudorule=sudocmdgroup)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
405
plugins/modules/identity/ipa/ipa_sudorule.py
Normal file
405
plugins/modules/identity/ipa/ipa_sudorule.py
Normal file
|
@ -0,0 +1,405 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_sudorule
|
||||
author: Thomas Krahn (@Nosmoht)
|
||||
short_description: Manage FreeIPA sudo rule
|
||||
description:
|
||||
- Add, modify or delete sudo rule within IPA server using IPA API.
|
||||
options:
|
||||
cn:
|
||||
description:
|
||||
- Canonical name.
|
||||
- Can not be changed as it is the unique identifier.
|
||||
required: true
|
||||
aliases: ['name']
|
||||
type: str
|
||||
cmdcategory:
|
||||
description:
|
||||
- Command category the rule applies to.
|
||||
choices: ['all']
|
||||
type: str
|
||||
cmd:
|
||||
description:
|
||||
- List of commands assigned to the rule.
|
||||
- If an empty list is passed all commands will be removed from the rule.
|
||||
- If option is omitted commands will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
description:
|
||||
description:
|
||||
- Description of the sudo rule.
|
||||
type: str
|
||||
host:
|
||||
description:
|
||||
- List of hosts assigned to the rule.
|
||||
- If an empty list is passed all hosts will be removed from the rule.
|
||||
- If option is omitted hosts will not be checked or changed.
|
||||
- Option C(hostcategory) must be omitted to assign hosts.
|
||||
type: list
|
||||
elements: str
|
||||
hostcategory:
|
||||
description:
|
||||
- Host category the rule applies to.
|
||||
- If 'all' is passed one must omit C(host) and C(hostgroup).
|
||||
- Option C(host) and C(hostgroup) must be omitted to assign 'all'.
|
||||
choices: ['all']
|
||||
type: str
|
||||
hostgroup:
|
||||
description:
|
||||
- List of host groups assigned to the rule.
|
||||
- If an empty list is passed all host groups will be removed from the rule.
|
||||
- If option is omitted host groups will not be checked or changed.
|
||||
- Option C(hostcategory) must be omitted to assign host groups.
|
||||
type: list
|
||||
elements: str
|
||||
runasusercategory:
|
||||
description:
|
||||
- RunAs User category the rule applies to.
|
||||
choices: ['all']
|
||||
type: str
|
||||
runasgroupcategory:
|
||||
description:
|
||||
- RunAs Group category the rule applies to.
|
||||
choices: ['all']
|
||||
type: str
|
||||
sudoopt:
|
||||
description:
|
||||
- List of options to add to the sudo rule.
|
||||
type: list
|
||||
elements: str
|
||||
user:
|
||||
description:
|
||||
- List of users assigned to the rule.
|
||||
- If an empty list is passed all users will be removed from the rule.
|
||||
- If option is omitted users will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
usercategory:
|
||||
description:
|
||||
- User category the rule applies to.
|
||||
choices: ['all']
|
||||
type: str
|
||||
usergroup:
|
||||
description:
|
||||
- List of user groups assigned to the rule.
|
||||
- If an empty list is passed all user groups will be removed from the rule.
|
||||
- If option is omitted user groups will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
state:
|
||||
description: State to ensure.
|
||||
default: present
|
||||
choices: ['absent', 'disabled', 'enabled', 'present']
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure sudo rule is present that's allows all every body to execute any command on any host without being asked for a password.
|
||||
ipa_sudorule:
|
||||
name: sudo_all_nopasswd
|
||||
cmdcategory: all
|
||||
description: Allow to run every command with sudo without password
|
||||
hostcategory: all
|
||||
sudoopt:
|
||||
- '!authenticate'
|
||||
usercategory: all
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure user group developers can run every command on host group db-server as well as on host db01.example.com.
|
||||
ipa_sudorule:
|
||||
name: sudo_dev_dbserver
|
||||
description: Allow developers to run every command with sudo on all database server
|
||||
cmdcategory: all
|
||||
host:
|
||||
- db01.example.com
|
||||
hostgroup:
|
||||
- db-server
|
||||
sudoopt:
|
||||
- '!authenticate'
|
||||
usergroup:
|
||||
- developers
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
sudorule:
|
||||
description: Sudorule as returned by IPA
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class SudoRuleIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(SudoRuleIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def sudorule_find(self, name):
|
||||
return self._post_json(method='sudorule_find', name=None, item={'all': True, 'cn': name})
|
||||
|
||||
def sudorule_add(self, name, item):
|
||||
return self._post_json(method='sudorule_add', name=name, item=item)
|
||||
|
||||
def sudorule_mod(self, name, item):
|
||||
return self._post_json(method='sudorule_mod', name=name, item=item)
|
||||
|
||||
def sudorule_del(self, name):
|
||||
return self._post_json(method='sudorule_del', name=name)
|
||||
|
||||
def sudorule_add_option(self, name, item):
|
||||
return self._post_json(method='sudorule_add_option', name=name, item=item)
|
||||
|
||||
def sudorule_add_option_ipasudoopt(self, name, item):
|
||||
return self.sudorule_add_option(name=name, item={'ipasudoopt': item})
|
||||
|
||||
def sudorule_remove_option(self, name, item):
|
||||
return self._post_json(method='sudorule_remove_option', name=name, item=item)
|
||||
|
||||
def sudorule_remove_option_ipasudoopt(self, name, item):
|
||||
return self.sudorule_remove_option(name=name, item={'ipasudoopt': item})
|
||||
|
||||
def sudorule_add_host(self, name, item):
|
||||
return self._post_json(method='sudorule_add_host', name=name, item=item)
|
||||
|
||||
def sudorule_add_host_host(self, name, item):
|
||||
return self.sudorule_add_host(name=name, item={'host': item})
|
||||
|
||||
def sudorule_add_host_hostgroup(self, name, item):
|
||||
return self.sudorule_add_host(name=name, item={'hostgroup': item})
|
||||
|
||||
def sudorule_remove_host(self, name, item):
|
||||
return self._post_json(method='sudorule_remove_host', name=name, item=item)
|
||||
|
||||
def sudorule_remove_host_host(self, name, item):
|
||||
return self.sudorule_remove_host(name=name, item={'host': item})
|
||||
|
||||
def sudorule_remove_host_hostgroup(self, name, item):
|
||||
return self.sudorule_remove_host(name=name, item={'hostgroup': item})
|
||||
|
||||
def sudorule_add_allow_command(self, name, item):
|
||||
return self._post_json(method='sudorule_add_allow_command', name=name, item={'sudocmd': item})
|
||||
|
||||
def sudorule_remove_allow_command(self, name, item):
|
||||
return self._post_json(method='sudorule_remove_allow_command', name=name, item=item)
|
||||
|
||||
def sudorule_add_user(self, name, item):
|
||||
return self._post_json(method='sudorule_add_user', name=name, item=item)
|
||||
|
||||
def sudorule_add_user_user(self, name, item):
|
||||
return self.sudorule_add_user(name=name, item={'user': item})
|
||||
|
||||
def sudorule_add_user_group(self, name, item):
|
||||
return self.sudorule_add_user(name=name, item={'group': item})
|
||||
|
||||
def sudorule_remove_user(self, name, item):
|
||||
return self._post_json(method='sudorule_remove_user', name=name, item=item)
|
||||
|
||||
def sudorule_remove_user_user(self, name, item):
|
||||
return self.sudorule_remove_user(name=name, item={'user': item})
|
||||
|
||||
def sudorule_remove_user_group(self, name, item):
|
||||
return self.sudorule_remove_user(name=name, item={'group': item})
|
||||
|
||||
|
||||
def get_sudorule_dict(cmdcategory=None, description=None, hostcategory=None, ipaenabledflag=None, usercategory=None,
|
||||
runasgroupcategory=None, runasusercategory=None):
|
||||
data = {}
|
||||
if cmdcategory is not None:
|
||||
data['cmdcategory'] = cmdcategory
|
||||
if description is not None:
|
||||
data['description'] = description
|
||||
if hostcategory is not None:
|
||||
data['hostcategory'] = hostcategory
|
||||
if ipaenabledflag is not None:
|
||||
data['ipaenabledflag'] = ipaenabledflag
|
||||
if usercategory is not None:
|
||||
data['usercategory'] = usercategory
|
||||
if runasusercategory is not None:
|
||||
data['ipasudorunasusercategory'] = runasusercategory
|
||||
if runasgroupcategory is not None:
|
||||
data['ipasudorunasgroupcategory'] = runasgroupcategory
|
||||
return data
|
||||
|
||||
|
||||
def category_changed(module, client, category_name, ipa_sudorule):
|
||||
if ipa_sudorule.get(category_name, None) == ['all']:
|
||||
if not module.check_mode:
|
||||
# cn is returned as list even with only a single value.
|
||||
client.sudorule_mod(name=ipa_sudorule.get('cn')[0], item={category_name: None})
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
state = module.params['state']
|
||||
name = module.params['cn']
|
||||
cmd = module.params['cmd']
|
||||
cmdcategory = module.params['cmdcategory']
|
||||
host = module.params['host']
|
||||
hostcategory = module.params['hostcategory']
|
||||
hostgroup = module.params['hostgroup']
|
||||
runasusercategory = module.params['runasusercategory']
|
||||
runasgroupcategory = module.params['runasgroupcategory']
|
||||
|
||||
if state in ['present', 'enabled']:
|
||||
ipaenabledflag = 'TRUE'
|
||||
else:
|
||||
ipaenabledflag = 'FALSE'
|
||||
|
||||
sudoopt = module.params['sudoopt']
|
||||
user = module.params['user']
|
||||
usercategory = module.params['usercategory']
|
||||
usergroup = module.params['usergroup']
|
||||
|
||||
module_sudorule = get_sudorule_dict(cmdcategory=cmdcategory,
|
||||
description=module.params['description'],
|
||||
hostcategory=hostcategory,
|
||||
ipaenabledflag=ipaenabledflag,
|
||||
usercategory=usercategory,
|
||||
runasusercategory=runasusercategory,
|
||||
runasgroupcategory=runasgroupcategory)
|
||||
ipa_sudorule = client.sudorule_find(name=name)
|
||||
|
||||
changed = False
|
||||
if state in ['present', 'disabled', 'enabled']:
|
||||
if not ipa_sudorule:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
ipa_sudorule = client.sudorule_add(name=name, item=module_sudorule)
|
||||
else:
|
||||
diff = client.get_diff(ipa_sudorule, module_sudorule)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
if 'hostcategory' in diff:
|
||||
if ipa_sudorule.get('memberhost_host', None) is not None:
|
||||
client.sudorule_remove_host_host(name=name, item=ipa_sudorule.get('memberhost_host'))
|
||||
if ipa_sudorule.get('memberhost_hostgroup', None) is not None:
|
||||
client.sudorule_remove_host_hostgroup(name=name,
|
||||
item=ipa_sudorule.get('memberhost_hostgroup'))
|
||||
|
||||
client.sudorule_mod(name=name, item=module_sudorule)
|
||||
|
||||
if cmd is not None:
|
||||
changed = category_changed(module, client, 'cmdcategory', ipa_sudorule) or changed
|
||||
if not module.check_mode:
|
||||
client.sudorule_add_allow_command(name=name, item=cmd)
|
||||
|
||||
if runasusercategory is not None:
|
||||
changed = category_changed(module, client, 'iparunasusercategory', ipa_sudorule) or changed
|
||||
|
||||
if runasgroupcategory is not None:
|
||||
changed = category_changed(module, client, 'iparunasgroupcategory', ipa_sudorule) or changed
|
||||
|
||||
if host is not None:
|
||||
changed = category_changed(module, client, 'hostcategory', ipa_sudorule) or changed
|
||||
changed = client.modify_if_diff(name, ipa_sudorule.get('memberhost_host', []), host,
|
||||
client.sudorule_add_host_host,
|
||||
client.sudorule_remove_host_host) or changed
|
||||
|
||||
if hostgroup is not None:
|
||||
changed = category_changed(module, client, 'hostcategory', ipa_sudorule) or changed
|
||||
changed = client.modify_if_diff(name, ipa_sudorule.get('memberhost_hostgroup', []), hostgroup,
|
||||
client.sudorule_add_host_hostgroup,
|
||||
client.sudorule_remove_host_hostgroup) or changed
|
||||
if sudoopt is not None:
|
||||
# client.modify_if_diff does not work as each option must be removed/added by its own
|
||||
ipa_list = ipa_sudorule.get('ipasudoopt', [])
|
||||
module_list = sudoopt
|
||||
diff = list(set(ipa_list) - set(module_list))
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
for item in diff:
|
||||
client.sudorule_remove_option_ipasudoopt(name, item)
|
||||
diff = list(set(module_list) - set(ipa_list))
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
for item in diff:
|
||||
client.sudorule_add_option_ipasudoopt(name, item)
|
||||
|
||||
if user is not None:
|
||||
changed = category_changed(module, client, 'usercategory', ipa_sudorule) or changed
|
||||
changed = client.modify_if_diff(name, ipa_sudorule.get('memberuser_user', []), user,
|
||||
client.sudorule_add_user_user,
|
||||
client.sudorule_remove_user_user) or changed
|
||||
if usergroup is not None:
|
||||
changed = category_changed(module, client, 'usercategory', ipa_sudorule) or changed
|
||||
changed = client.modify_if_diff(name, ipa_sudorule.get('memberuser_group', []), usergroup,
|
||||
client.sudorule_add_user_group,
|
||||
client.sudorule_remove_user_group) or changed
|
||||
else:
|
||||
if ipa_sudorule:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.sudorule_del(name)
|
||||
|
||||
return changed, client.sudorule_find(name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(cmd=dict(type='list', elements='str'),
|
||||
cmdcategory=dict(type='str', choices=['all']),
|
||||
cn=dict(type='str', required=True, aliases=['name']),
|
||||
description=dict(type='str'),
|
||||
host=dict(type='list', elements='str'),
|
||||
hostcategory=dict(type='str', choices=['all']),
|
||||
hostgroup=dict(type='list', elements='str'),
|
||||
runasusercategory=dict(type='str', choices=['all']),
|
||||
runasgroupcategory=dict(type='str', choices=['all']),
|
||||
sudoopt=dict(type='list', elements='str'),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
|
||||
user=dict(type='list', elements='str'),
|
||||
usercategory=dict(type='str', choices=['all']),
|
||||
usergroup=dict(type='list', elements='str'))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
mutually_exclusive=[['cmdcategory', 'cmd'],
|
||||
['hostcategory', 'host'],
|
||||
['hostcategory', 'hostgroup'],
|
||||
['usercategory', 'user'],
|
||||
['usercategory', 'usergroup']],
|
||||
supports_check_mode=True)
|
||||
|
||||
client = SudoRuleIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, sudorule = ensure(module, client)
|
||||
module.exit_json(changed=changed, sudorule=sudorule)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
374
plugins/modules/identity/ipa/ipa_user.py
Normal file
374
plugins/modules/identity/ipa/ipa_user.py
Normal file
|
@ -0,0 +1,374 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_user
|
||||
author: Thomas Krahn (@Nosmoht)
|
||||
short_description: Manage FreeIPA users
|
||||
description:
|
||||
- Add, modify and delete user within IPA server.
|
||||
options:
|
||||
displayname:
|
||||
description: Display name.
|
||||
type: str
|
||||
update_password:
|
||||
description:
|
||||
- Set password for a user.
|
||||
type: str
|
||||
default: 'always'
|
||||
choices: [ always, on_create ]
|
||||
givenname:
|
||||
description: First name.
|
||||
type: str
|
||||
krbpasswordexpiration:
|
||||
description:
|
||||
- Date at which the user password will expire.
|
||||
- In the format YYYYMMddHHmmss.
|
||||
- e.g. 20180121182022 will expire on 21 January 2018 at 18:20:22.
|
||||
type: str
|
||||
loginshell:
|
||||
description: Login shell.
|
||||
type: str
|
||||
mail:
|
||||
description:
|
||||
- List of mail addresses assigned to the user.
|
||||
- If an empty list is passed all assigned email addresses will be deleted.
|
||||
- If None is passed email addresses will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
password:
|
||||
description:
|
||||
- Password for a user.
|
||||
- Will not be set for an existing user unless I(update_password=always), which is the default.
|
||||
type: str
|
||||
sn:
|
||||
description: Surname.
|
||||
type: str
|
||||
sshpubkey:
|
||||
description:
|
||||
- List of public SSH key.
|
||||
- If an empty list is passed all assigned public keys will be deleted.
|
||||
- If None is passed SSH public keys will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
state:
|
||||
description: State to ensure.
|
||||
default: "present"
|
||||
choices: ["absent", "disabled", "enabled", "present"]
|
||||
type: str
|
||||
telephonenumber:
|
||||
description:
|
||||
- List of telephone numbers assigned to the user.
|
||||
- If an empty list is passed all assigned telephone numbers will be deleted.
|
||||
- If None is passed telephone numbers will not be checked or changed.
|
||||
type: list
|
||||
elements: str
|
||||
title:
|
||||
description: Title.
|
||||
type: str
|
||||
uid:
|
||||
description: uid of the user.
|
||||
required: true
|
||||
aliases: ["name"]
|
||||
type: str
|
||||
uidnumber:
|
||||
description:
|
||||
- Account Settings UID/Posix User ID number.
|
||||
type: str
|
||||
gidnumber:
|
||||
description:
|
||||
- Posix Group ID.
|
||||
type: str
|
||||
homedirectory:
|
||||
description:
|
||||
- Default home directory of the user.
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
requirements:
|
||||
- base64
|
||||
- hashlib
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure pinky is present and always reset password
|
||||
ipa_user:
|
||||
name: pinky
|
||||
state: present
|
||||
krbpasswordexpiration: 20200119235959
|
||||
givenname: Pinky
|
||||
sn: Acme
|
||||
mail:
|
||||
- pinky@acme.com
|
||||
telephonenumber:
|
||||
- '+555123456'
|
||||
sshpubkey:
|
||||
- ssh-rsa ....
|
||||
- ssh-dsa ....
|
||||
uidnumber: 1001
|
||||
gidnumber: 100
|
||||
homedirectory: /home/pinky
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure brain is absent
|
||||
ipa_user:
|
||||
name: brain
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure pinky is present but don't reset password if already exists
|
||||
ipa_user:
|
||||
name: pinky
|
||||
state: present
|
||||
givenname: Pinky
|
||||
sn: Acme
|
||||
password: zounds
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
update_password: on_create
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
user:
|
||||
description: User as returned by IPA API
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class UserIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(UserIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def user_find(self, name):
|
||||
return self._post_json(method='user_find', name=None, item={'all': True, 'uid': name})
|
||||
|
||||
def user_add(self, name, item):
|
||||
return self._post_json(method='user_add', name=name, item=item)
|
||||
|
||||
def user_mod(self, name, item):
|
||||
return self._post_json(method='user_mod', name=name, item=item)
|
||||
|
||||
def user_del(self, name):
|
||||
return self._post_json(method='user_del', name=name)
|
||||
|
||||
def user_disable(self, name):
|
||||
return self._post_json(method='user_disable', name=name)
|
||||
|
||||
def user_enable(self, name):
|
||||
return self._post_json(method='user_enable', name=name)
|
||||
|
||||
|
||||
def get_user_dict(displayname=None, givenname=None, krbpasswordexpiration=None, loginshell=None,
|
||||
mail=None, nsaccountlock=False, sn=None, sshpubkey=None, telephonenumber=None,
|
||||
title=None, userpassword=None, gidnumber=None, uidnumber=None, homedirectory=None):
|
||||
user = {}
|
||||
if displayname is not None:
|
||||
user['displayname'] = displayname
|
||||
if krbpasswordexpiration is not None:
|
||||
user['krbpasswordexpiration'] = krbpasswordexpiration + "Z"
|
||||
if givenname is not None:
|
||||
user['givenname'] = givenname
|
||||
if loginshell is not None:
|
||||
user['loginshell'] = loginshell
|
||||
if mail is not None:
|
||||
user['mail'] = mail
|
||||
user['nsaccountlock'] = nsaccountlock
|
||||
if sn is not None:
|
||||
user['sn'] = sn
|
||||
if sshpubkey is not None:
|
||||
user['ipasshpubkey'] = sshpubkey
|
||||
if telephonenumber is not None:
|
||||
user['telephonenumber'] = telephonenumber
|
||||
if title is not None:
|
||||
user['title'] = title
|
||||
if userpassword is not None:
|
||||
user['userpassword'] = userpassword
|
||||
if gidnumber is not None:
|
||||
user['gidnumber'] = gidnumber
|
||||
if uidnumber is not None:
|
||||
user['uidnumber'] = uidnumber
|
||||
if homedirectory is not None:
|
||||
user['homedirectory'] = homedirectory
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def get_user_diff(client, ipa_user, module_user):
|
||||
"""
|
||||
Return the keys of each dict whereas values are different. Unfortunately the IPA
|
||||
API returns everything as a list even if only a single value is possible.
|
||||
Therefore some more complexity is needed.
|
||||
The method will check if the value type of module_user.attr is not a list and
|
||||
create a list with that element if the same attribute in ipa_user is list. In this way I hope that the method
|
||||
must not be changed if the returned API dict is changed.
|
||||
:param ipa_user:
|
||||
:param module_user:
|
||||
:return:
|
||||
"""
|
||||
# sshpubkeyfp is the list of ssh key fingerprints. IPA doesn't return the keys itself but instead the fingerprints.
|
||||
# These are used for comparison.
|
||||
sshpubkey = None
|
||||
if 'ipasshpubkey' in module_user:
|
||||
hash_algo = 'md5'
|
||||
if 'sshpubkeyfp' in ipa_user and ipa_user['sshpubkeyfp'][0][:7].upper() == 'SHA256:':
|
||||
hash_algo = 'sha256'
|
||||
module_user['sshpubkeyfp'] = [get_ssh_key_fingerprint(pubkey, hash_algo) for pubkey in module_user['ipasshpubkey']]
|
||||
# Remove the ipasshpubkey element as it is not returned from IPA but save it's value to be used later on
|
||||
sshpubkey = module_user['ipasshpubkey']
|
||||
del module_user['ipasshpubkey']
|
||||
|
||||
result = client.get_diff(ipa_data=ipa_user, module_data=module_user)
|
||||
|
||||
# If there are public keys, remove the fingerprints and add them back to the dict
|
||||
if sshpubkey is not None:
|
||||
del module_user['sshpubkeyfp']
|
||||
module_user['ipasshpubkey'] = sshpubkey
|
||||
return result
|
||||
|
||||
|
||||
def get_ssh_key_fingerprint(ssh_key, hash_algo='sha256'):
|
||||
"""
|
||||
Return the public key fingerprint of a given public SSH key
|
||||
in format "[fp] [user@host] (ssh-rsa)" where fp is of the format:
|
||||
FB:0C:AC:0A:07:94:5B:CE:75:6E:63:32:13:AD:AD:D7
|
||||
for md5 or
|
||||
SHA256:[base64]
|
||||
for sha256
|
||||
:param ssh_key:
|
||||
:param hash_algo:
|
||||
:return:
|
||||
"""
|
||||
parts = ssh_key.strip().split()
|
||||
if len(parts) == 0:
|
||||
return None
|
||||
key_type = parts[0]
|
||||
key = base64.b64decode(parts[1].encode('ascii'))
|
||||
|
||||
if hash_algo == 'md5':
|
||||
fp_plain = hashlib.md5(key).hexdigest()
|
||||
key_fp = ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])).upper()
|
||||
elif hash_algo == 'sha256':
|
||||
fp_plain = base64.b64encode(hashlib.sha256(key).digest()).decode('ascii').rstrip('=')
|
||||
key_fp = 'SHA256:{fp}'.format(fp=fp_plain)
|
||||
if len(parts) < 3:
|
||||
return "%s (%s)" % (key_fp, key_type)
|
||||
else:
|
||||
user_host = parts[2]
|
||||
return "%s %s (%s)" % (key_fp, user_host, key_type)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
state = module.params['state']
|
||||
name = module.params['uid']
|
||||
nsaccountlock = state == 'disabled'
|
||||
|
||||
module_user = get_user_dict(displayname=module.params.get('displayname'),
|
||||
krbpasswordexpiration=module.params.get('krbpasswordexpiration'),
|
||||
givenname=module.params.get('givenname'),
|
||||
loginshell=module.params['loginshell'],
|
||||
mail=module.params['mail'], sn=module.params['sn'],
|
||||
sshpubkey=module.params['sshpubkey'], nsaccountlock=nsaccountlock,
|
||||
telephonenumber=module.params['telephonenumber'], title=module.params['title'],
|
||||
userpassword=module.params['password'],
|
||||
gidnumber=module.params.get('gidnumber'), uidnumber=module.params.get('uidnumber'),
|
||||
homedirectory=module.params.get('homedirectory'))
|
||||
|
||||
update_password = module.params.get('update_password')
|
||||
ipa_user = client.user_find(name=name)
|
||||
|
||||
changed = False
|
||||
if state in ['present', 'enabled', 'disabled']:
|
||||
if not ipa_user:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
ipa_user = client.user_add(name=name, item=module_user)
|
||||
else:
|
||||
if update_password == 'on_create':
|
||||
module_user.pop('userpassword', None)
|
||||
diff = get_user_diff(client, ipa_user, module_user)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
ipa_user = client.user_mod(name=name, item=module_user)
|
||||
else:
|
||||
if ipa_user:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.user_del(name)
|
||||
|
||||
return changed, ipa_user
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(displayname=dict(type='str'),
|
||||
givenname=dict(type='str'),
|
||||
update_password=dict(type='str', default="always",
|
||||
choices=['always', 'on_create']),
|
||||
krbpasswordexpiration=dict(type='str'),
|
||||
loginshell=dict(type='str'),
|
||||
mail=dict(type='list', elements='str'),
|
||||
sn=dict(type='str'),
|
||||
uid=dict(type='str', required=True, aliases=['name']),
|
||||
gidnumber=dict(type='str'),
|
||||
uidnumber=dict(type='str'),
|
||||
password=dict(type='str', no_log=True),
|
||||
sshpubkey=dict(type='list', elements='str'),
|
||||
state=dict(type='str', default='present',
|
||||
choices=['present', 'absent', 'enabled', 'disabled']),
|
||||
telephonenumber=dict(type='list', elements='str'),
|
||||
title=dict(type='str'),
|
||||
homedirectory=dict(type='str'))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
client = UserIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
|
||||
# If sshpubkey is defined as None than module.params['sshpubkey'] is [None]. IPA itself returns None (not a list).
|
||||
# Therefore a small check here to replace list(None) by None. Otherwise get_user_diff() would return sshpubkey
|
||||
# as different which should be avoided.
|
||||
if module.params['sshpubkey'] is not None:
|
||||
if len(module.params['sshpubkey']) == 1 and module.params['sshpubkey'][0] == "":
|
||||
module.params['sshpubkey'] = None
|
||||
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, user = ensure(module, client)
|
||||
module.exit_json(changed=changed, user=user)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
253
plugins/modules/identity/ipa/ipa_vault.py
Normal file
253
plugins/modules/identity/ipa/ipa_vault.py
Normal file
|
@ -0,0 +1,253 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2018, Juan Manuel Parrilla <jparrill@redhat.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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: ipa_vault
|
||||
author: Juan Manuel Parrilla (@jparrill)
|
||||
short_description: Manage FreeIPA vaults
|
||||
description:
|
||||
- Add, modify and delete vaults and secret vaults.
|
||||
- KRA service should be enabled to use this module.
|
||||
options:
|
||||
cn:
|
||||
description:
|
||||
- Vault name.
|
||||
- Can not be changed as it is the unique identifier.
|
||||
required: true
|
||||
aliases: ["name"]
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- Description.
|
||||
type: str
|
||||
ipavaulttype:
|
||||
description:
|
||||
- Vault types are based on security level.
|
||||
default: "symmetric"
|
||||
choices: ["asymmetric", "standard", "symmetric"]
|
||||
aliases: ["vault_type"]
|
||||
type: str
|
||||
ipavaultpublickey:
|
||||
description:
|
||||
- Public key.
|
||||
aliases: ["vault_public_key"]
|
||||
type: str
|
||||
ipavaultsalt:
|
||||
description:
|
||||
- Vault Salt.
|
||||
aliases: ["vault_salt"]
|
||||
type: str
|
||||
username:
|
||||
description:
|
||||
- Any user can own one or more user vaults.
|
||||
- Mutually exclusive with service.
|
||||
aliases: ["user"]
|
||||
type: list
|
||||
elements: str
|
||||
service:
|
||||
description:
|
||||
- Any service can own one or more service vaults.
|
||||
- Mutually exclusive with user.
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- State to ensure.
|
||||
default: "present"
|
||||
choices: ["absent", "present"]
|
||||
type: str
|
||||
replace:
|
||||
description:
|
||||
- Force replace the existant vault on IPA server.
|
||||
type: bool
|
||||
default: False
|
||||
choices: ["True", "False"]
|
||||
validate_certs:
|
||||
description:
|
||||
- Validate IPA server certificates.
|
||||
type: bool
|
||||
default: true
|
||||
extends_documentation_fragment:
|
||||
- community.general.ipa.documentation
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure vault is present
|
||||
ipa_vault:
|
||||
name: vault01
|
||||
vault_type: standard
|
||||
user: user01
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
validate_certs: false
|
||||
|
||||
- name: Ensure vault is present for Admin user
|
||||
ipa_vault:
|
||||
name: vault01
|
||||
vault_type: standard
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Ensure vault is absent
|
||||
ipa_vault:
|
||||
name: vault01
|
||||
vault_type: standard
|
||||
user: user01
|
||||
state: absent
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
|
||||
- name: Modify vault if already exists
|
||||
ipa_vault:
|
||||
name: vault01
|
||||
vault_type: standard
|
||||
description: "Vault for test"
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
replace: True
|
||||
|
||||
- name: Get vault info if already exists
|
||||
ipa_vault:
|
||||
name: vault01
|
||||
ipa_host: ipa.example.com
|
||||
ipa_user: admin
|
||||
ipa_pass: topsecret
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
vault:
|
||||
description: Vault as returned by IPA API
|
||||
returned: always
|
||||
type: dict
|
||||
'''
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class VaultIPAClient(IPAClient):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
super(VaultIPAClient, self).__init__(module, host, port, protocol)
|
||||
|
||||
def vault_find(self, name):
|
||||
return self._post_json(method='vault_find', name=None, item={'all': True, 'cn': name})
|
||||
|
||||
def vault_add_internal(self, name, item):
|
||||
return self._post_json(method='vault_add_internal', name=name, item=item)
|
||||
|
||||
def vault_mod_internal(self, name, item):
|
||||
return self._post_json(method='vault_mod_internal', name=name, item=item)
|
||||
|
||||
def vault_del(self, name):
|
||||
return self._post_json(method='vault_del', name=name)
|
||||
|
||||
|
||||
def get_vault_dict(description=None, vault_type=None, vault_salt=None, vault_public_key=None, service=None):
|
||||
vault = {}
|
||||
|
||||
if description is not None:
|
||||
vault['description'] = description
|
||||
if vault_type is not None:
|
||||
vault['ipavaulttype'] = vault_type
|
||||
if vault_salt is not None:
|
||||
vault['ipavaultsalt'] = vault_salt
|
||||
if vault_public_key is not None:
|
||||
vault['ipavaultpublickey'] = vault_public_key
|
||||
if service is not None:
|
||||
vault['service'] = service
|
||||
return vault
|
||||
|
||||
|
||||
def get_vault_diff(client, ipa_vault, module_vault, module):
|
||||
return client.get_diff(ipa_data=ipa_vault, module_data=module_vault)
|
||||
|
||||
|
||||
def ensure(module, client):
|
||||
state = module.params['state']
|
||||
name = module.params['cn']
|
||||
user = module.params['username']
|
||||
replace = module.params['replace']
|
||||
|
||||
module_vault = get_vault_dict(description=module.params['description'], vault_type=module.params['ipavaulttype'],
|
||||
vault_salt=module.params['ipavaultsalt'],
|
||||
vault_public_key=module.params['ipavaultpublickey'],
|
||||
service=module.params['service'])
|
||||
ipa_vault = client.vault_find(name=name)
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not ipa_vault:
|
||||
# New vault
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
ipa_vault = client.vault_add_internal(name, item=module_vault)
|
||||
else:
|
||||
# Already exists
|
||||
if replace:
|
||||
diff = get_vault_diff(client, ipa_vault, module_vault, module)
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
data = {}
|
||||
for key in diff:
|
||||
data[key] = module_vault.get(key)
|
||||
client.vault_mod_internal(name=name, item=data)
|
||||
|
||||
else:
|
||||
if ipa_vault:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
client.vault_del(name)
|
||||
|
||||
return changed, client.vault_find(name=name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ipa_argument_spec()
|
||||
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
|
||||
description=dict(type='str'),
|
||||
ipavaulttype=dict(type='str', default='symmetric',
|
||||
choices=['standard', 'symmetric', 'asymmetric'], aliases=['vault_type']),
|
||||
ipavaultsalt=dict(type='str', aliases=['vault_salt']),
|
||||
ipavaultpublickey=dict(type='str', aliases=['vault_public_key']),
|
||||
service=dict(type='str'),
|
||||
replace=dict(type='bool', default=False, choices=[True, False]),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
username=dict(type='list', elements='str', aliases=['user']))
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[['username', 'service']])
|
||||
|
||||
client = VaultIPAClient(module=module,
|
||||
host=module.params['ipa_host'],
|
||||
port=module.params['ipa_port'],
|
||||
protocol=module.params['ipa_prot'])
|
||||
try:
|
||||
client.login(username=module.params['ipa_user'],
|
||||
password=module.params['ipa_pass'])
|
||||
changed, vault = ensure(module, client)
|
||||
module.exit_json(changed=changed, vault=vault)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
853
plugins/modules/identity/keycloak/keycloak_client.py
Normal file
853
plugins/modules/identity/keycloak/keycloak_client.py
Normal file
|
@ -0,0 +1,853 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: keycloak_client
|
||||
|
||||
short_description: Allows administration of Keycloak clients via Keycloak API
|
||||
|
||||
|
||||
description:
|
||||
- This module allows the administration of Keycloak clients via the Keycloak REST API. It
|
||||
requires access to the REST API via OpenID Connect; the user connecting and the client being
|
||||
used must have the requisite access rights. In a default Keycloak installation, admin-cli
|
||||
and an admin user would work, as would a separate client definition with the scope tailored
|
||||
to your needs and a user having the expected roles.
|
||||
|
||||
- The names of module options are snake_cased versions of the camelCase ones found in the
|
||||
Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html).
|
||||
Aliases are provided so camelCased versions can be used as well.
|
||||
|
||||
- The Keycloak API does not always sanity check inputs e.g. you can set
|
||||
SAML-specific settings on an OpenID Connect client for instance and vice versa. Be careful.
|
||||
If you do not specify a setting, usually a sensible default is chosen.
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the client
|
||||
- On C(present), the client will be created (or updated if it exists already).
|
||||
- On C(absent), the client will be removed if it exists
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
|
||||
realm:
|
||||
description:
|
||||
- The realm to create the client in.
|
||||
|
||||
client_id:
|
||||
description:
|
||||
- Client id of client to be worked on. This is usually an alphanumeric name chosen by
|
||||
you. Either this or I(id) is required. If you specify both, I(id) takes precedence.
|
||||
This is 'clientId' in the Keycloak REST API.
|
||||
aliases:
|
||||
- clientId
|
||||
|
||||
id:
|
||||
description:
|
||||
- Id of client to be worked on. This is usually an UUID. Either this or I(client_id)
|
||||
is required. If you specify both, this takes precedence.
|
||||
|
||||
name:
|
||||
description:
|
||||
- Name of the client (this is not the same as I(client_id))
|
||||
|
||||
description:
|
||||
description:
|
||||
- Description of the client in Keycloak
|
||||
|
||||
root_url:
|
||||
description:
|
||||
- Root URL appended to relative URLs for this client
|
||||
This is 'rootUrl' in the Keycloak REST API.
|
||||
aliases:
|
||||
- rootUrl
|
||||
|
||||
admin_url:
|
||||
description:
|
||||
- URL to the admin interface of the client
|
||||
This is 'adminUrl' in the Keycloak REST API.
|
||||
aliases:
|
||||
- adminUrl
|
||||
|
||||
base_url:
|
||||
description:
|
||||
- Default URL to use when the auth server needs to redirect or link back to the client
|
||||
This is 'baseUrl' in the Keycloak REST API.
|
||||
aliases:
|
||||
- baseUrl
|
||||
|
||||
enabled:
|
||||
description:
|
||||
- Is this client enabled or not?
|
||||
type: bool
|
||||
|
||||
client_authenticator_type:
|
||||
description:
|
||||
- How do clients authenticate with the auth server? Either C(client-secret) or
|
||||
C(client-jwt) can be chosen. When using C(client-secret), the module parameter
|
||||
I(secret) can set it, while for C(client-jwt), you can use the keys C(use.jwks.url),
|
||||
C(jwks.url), and C(jwt.credential.certificate) in the I(attributes) module parameter
|
||||
to configure its behavior.
|
||||
This is 'clientAuthenticatorType' in the Keycloak REST API.
|
||||
choices: ['client-secret', 'client-jwt']
|
||||
aliases:
|
||||
- clientAuthenticatorType
|
||||
|
||||
secret:
|
||||
description:
|
||||
- When using I(client_authenticator_type) C(client-secret) (the default), you can
|
||||
specify a secret here (otherwise one will be generated if it does not exit). If
|
||||
changing this secret, the module will not register a change currently (but the
|
||||
changed secret will be saved).
|
||||
|
||||
registration_access_token:
|
||||
description:
|
||||
- The registration access token provides access for clients to the client registration
|
||||
service.
|
||||
This is 'registrationAccessToken' in the Keycloak REST API.
|
||||
aliases:
|
||||
- registrationAccessToken
|
||||
|
||||
default_roles:
|
||||
description:
|
||||
- list of default roles for this client. If the client roles referenced do not exist
|
||||
yet, they will be created.
|
||||
This is 'defaultRoles' in the Keycloak REST API.
|
||||
aliases:
|
||||
- defaultRoles
|
||||
|
||||
redirect_uris:
|
||||
description:
|
||||
- Acceptable redirect URIs for this client.
|
||||
This is 'redirectUris' in the Keycloak REST API.
|
||||
aliases:
|
||||
- redirectUris
|
||||
|
||||
web_origins:
|
||||
description:
|
||||
- List of allowed CORS origins.
|
||||
This is 'webOrigins' in the Keycloak REST API.
|
||||
aliases:
|
||||
- webOrigins
|
||||
|
||||
not_before:
|
||||
description:
|
||||
- Revoke any tokens issued before this date for this client (this is a UNIX timestamp).
|
||||
This is 'notBefore' in the Keycloak REST API.
|
||||
aliases:
|
||||
- notBefore
|
||||
|
||||
bearer_only:
|
||||
description:
|
||||
- The access type of this client is bearer-only.
|
||||
This is 'bearerOnly' in the Keycloak REST API.
|
||||
aliases:
|
||||
- bearerOnly
|
||||
type: bool
|
||||
|
||||
consent_required:
|
||||
description:
|
||||
- If enabled, users have to consent to client access.
|
||||
This is 'consentRequired' in the Keycloak REST API.
|
||||
aliases:
|
||||
- consentRequired
|
||||
type: bool
|
||||
|
||||
standard_flow_enabled:
|
||||
description:
|
||||
- Enable standard flow for this client or not (OpenID connect).
|
||||
This is 'standardFlowEnabled' in the Keycloak REST API.
|
||||
aliases:
|
||||
- standardFlowEnabled
|
||||
type: bool
|
||||
|
||||
implicit_flow_enabled:
|
||||
description:
|
||||
- Enable implicit flow for this client or not (OpenID connect).
|
||||
This is 'implicitFlowEnabled' in the Keycloak REST API.
|
||||
aliases:
|
||||
- implicitFlowEnabled
|
||||
type: bool
|
||||
|
||||
direct_access_grants_enabled:
|
||||
description:
|
||||
- Are direct access grants enabled for this client or not (OpenID connect).
|
||||
This is 'directAccessGrantsEnabled' in the Keycloak REST API.
|
||||
aliases:
|
||||
- directAccessGrantsEnabled
|
||||
type: bool
|
||||
|
||||
service_accounts_enabled:
|
||||
description:
|
||||
- Are service accounts enabled for this client or not (OpenID connect).
|
||||
This is 'serviceAccountsEnabled' in the Keycloak REST API.
|
||||
aliases:
|
||||
- serviceAccountsEnabled
|
||||
type: bool
|
||||
|
||||
authorization_services_enabled:
|
||||
description:
|
||||
- Are authorization services enabled for this client or not (OpenID connect).
|
||||
This is 'authorizationServicesEnabled' in the Keycloak REST API.
|
||||
aliases:
|
||||
- authorizationServicesEnabled
|
||||
type: bool
|
||||
|
||||
public_client:
|
||||
description:
|
||||
- Is the access type for this client public or not.
|
||||
This is 'publicClient' in the Keycloak REST API.
|
||||
aliases:
|
||||
- publicClient
|
||||
type: bool
|
||||
|
||||
frontchannel_logout:
|
||||
description:
|
||||
- Is frontchannel logout enabled for this client or not.
|
||||
This is 'frontchannelLogout' in the Keycloak REST API.
|
||||
aliases:
|
||||
- frontchannelLogout
|
||||
type: bool
|
||||
|
||||
protocol:
|
||||
description:
|
||||
- Type of client (either C(openid-connect) or C(saml).
|
||||
choices: ['openid-connect', 'saml']
|
||||
|
||||
full_scope_allowed:
|
||||
description:
|
||||
- Is the "Full Scope Allowed" feature set for this client or not.
|
||||
This is 'fullScopeAllowed' in the Keycloak REST API.
|
||||
aliases:
|
||||
- fullScopeAllowed
|
||||
type: bool
|
||||
|
||||
node_re_registration_timeout:
|
||||
description:
|
||||
- Cluster node re-registration timeout for this client.
|
||||
This is 'nodeReRegistrationTimeout' in the Keycloak REST API.
|
||||
aliases:
|
||||
- nodeReRegistrationTimeout
|
||||
|
||||
registered_nodes:
|
||||
description:
|
||||
- dict of registered cluster nodes (with C(nodename) as the key and last registration
|
||||
time as the value).
|
||||
This is 'registeredNodes' in the Keycloak REST API.
|
||||
aliases:
|
||||
- registeredNodes
|
||||
|
||||
client_template:
|
||||
description:
|
||||
- Client template to use for this client. If it does not exist this field will silently
|
||||
be dropped.
|
||||
This is 'clientTemplate' in the Keycloak REST API.
|
||||
aliases:
|
||||
- clientTemplate
|
||||
|
||||
use_template_config:
|
||||
description:
|
||||
- Whether or not to use configuration from the I(client_template).
|
||||
This is 'useTemplateConfig' in the Keycloak REST API.
|
||||
aliases:
|
||||
- useTemplateConfig
|
||||
type: bool
|
||||
|
||||
use_template_scope:
|
||||
description:
|
||||
- Whether or not to use scope configuration from the I(client_template).
|
||||
This is 'useTemplateScope' in the Keycloak REST API.
|
||||
aliases:
|
||||
- useTemplateScope
|
||||
type: bool
|
||||
|
||||
use_template_mappers:
|
||||
description:
|
||||
- Whether or not to use mapper configuration from the I(client_template).
|
||||
This is 'useTemplateMappers' in the Keycloak REST API.
|
||||
aliases:
|
||||
- useTemplateMappers
|
||||
type: bool
|
||||
|
||||
surrogate_auth_required:
|
||||
description:
|
||||
- Whether or not surrogate auth is required.
|
||||
This is 'surrogateAuthRequired' in the Keycloak REST API.
|
||||
aliases:
|
||||
- surrogateAuthRequired
|
||||
type: bool
|
||||
|
||||
authorization_settings:
|
||||
description:
|
||||
- a data structure defining the authorization settings for this client. For reference,
|
||||
please see the Keycloak API docs at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_resourceserverrepresentation).
|
||||
This is 'authorizationSettings' in the Keycloak REST API.
|
||||
aliases:
|
||||
- authorizationSettings
|
||||
|
||||
protocol_mappers:
|
||||
description:
|
||||
- a list of dicts defining protocol mappers for this client.
|
||||
This is 'protocolMappers' in the Keycloak REST API.
|
||||
aliases:
|
||||
- protocolMappers
|
||||
suboptions:
|
||||
consentRequired:
|
||||
description:
|
||||
- Specifies whether a user needs to provide consent to a client for this mapper to be active.
|
||||
|
||||
consentText:
|
||||
description:
|
||||
- The human-readable name of the consent the user is presented to accept.
|
||||
|
||||
id:
|
||||
description:
|
||||
- Usually a UUID specifying the internal ID of this protocol mapper instance.
|
||||
|
||||
name:
|
||||
description:
|
||||
- The name of this protocol mapper.
|
||||
|
||||
protocol:
|
||||
description:
|
||||
- This is either C(openid-connect) or C(saml), this specifies for which protocol this protocol mapper
|
||||
is active.
|
||||
choices: ['openid-connect', 'saml']
|
||||
|
||||
protocolMapper:
|
||||
description:
|
||||
- The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is
|
||||
impossible to provide since this may be extended through SPIs by the user of Keycloak,
|
||||
by default Keycloak as of 3.4 ships with at least
|
||||
- C(docker-v2-allow-all-mapper)
|
||||
- C(oidc-address-mapper)
|
||||
- C(oidc-full-name-mapper)
|
||||
- C(oidc-group-membership-mapper)
|
||||
- C(oidc-hardcoded-claim-mapper)
|
||||
- C(oidc-hardcoded-role-mapper)
|
||||
- C(oidc-role-name-mapper)
|
||||
- C(oidc-script-based-protocol-mapper)
|
||||
- C(oidc-sha256-pairwise-sub-mapper)
|
||||
- C(oidc-usermodel-attribute-mapper)
|
||||
- C(oidc-usermodel-client-role-mapper)
|
||||
- C(oidc-usermodel-property-mapper)
|
||||
- C(oidc-usermodel-realm-role-mapper)
|
||||
- C(oidc-usersessionmodel-note-mapper)
|
||||
- C(saml-group-membership-mapper)
|
||||
- C(saml-hardcode-attribute-mapper)
|
||||
- C(saml-hardcode-role-mapper)
|
||||
- C(saml-role-list-mapper)
|
||||
- C(saml-role-name-mapper)
|
||||
- C(saml-user-attribute-mapper)
|
||||
- C(saml-user-property-mapper)
|
||||
- C(saml-user-session-note-mapper)
|
||||
- An exhaustive list of available mappers on your installation can be obtained on
|
||||
the admin console by going to Server Info -> Providers and looking under
|
||||
'protocol-mapper'.
|
||||
|
||||
config:
|
||||
description:
|
||||
- Dict specifying the configuration options for the protocol mapper; the
|
||||
contents differ depending on the value of I(protocolMapper) and are not documented
|
||||
other than by the source of the mappers and its parent class(es). An example is given
|
||||
below. It is easiest to obtain valid config values by dumping an already-existing
|
||||
protocol mapper configuration through check-mode in the I(existing) field.
|
||||
|
||||
attributes:
|
||||
description:
|
||||
- A dict of further attributes for this client. This can contain various configuration
|
||||
settings; an example is given in the examples section. While an exhaustive list of
|
||||
permissible options is not available; possible options as of Keycloak 3.4 are listed below. The Keycloak
|
||||
API does not validate whether a given option is appropriate for the protocol used; if specified
|
||||
anyway, Keycloak will simply not use it.
|
||||
suboptions:
|
||||
saml.authnstatement:
|
||||
description:
|
||||
- For SAML clients, boolean specifying whether or not a statement containing method and timestamp
|
||||
should be included in the login response.
|
||||
|
||||
saml.client.signature:
|
||||
description:
|
||||
- For SAML clients, boolean specifying whether a client signature is required and validated.
|
||||
|
||||
saml.encrypt:
|
||||
description:
|
||||
- Boolean specifying whether SAML assertions should be encrypted with the client's public key.
|
||||
|
||||
saml.force.post.binding:
|
||||
description:
|
||||
- For SAML clients, boolean specifying whether always to use POST binding for responses.
|
||||
|
||||
saml.onetimeuse.condition:
|
||||
description:
|
||||
- For SAML clients, boolean specifying whether a OneTimeUse condition should be included in login responses.
|
||||
|
||||
saml.server.signature:
|
||||
description:
|
||||
- Boolean specifying whether SAML documents should be signed by the realm.
|
||||
|
||||
saml.server.signature.keyinfo.ext:
|
||||
description:
|
||||
- For SAML clients, boolean specifying whether REDIRECT signing key lookup should be optimized through inclusion
|
||||
of the signing key id in the SAML Extensions element.
|
||||
|
||||
saml.signature.algorithm:
|
||||
description:
|
||||
- Signature algorithm used to sign SAML documents. One of C(RSA_SHA256), C(RSA_SHA1), C(RSA_SHA512), or C(DSA_SHA1).
|
||||
|
||||
saml.signing.certificate:
|
||||
description:
|
||||
- SAML signing key certificate, base64-encoded.
|
||||
|
||||
saml.signing.private.key:
|
||||
description:
|
||||
- SAML signing key private key, base64-encoded.
|
||||
|
||||
saml_assertion_consumer_url_post:
|
||||
description:
|
||||
- SAML POST Binding URL for the client's assertion consumer service (login responses).
|
||||
|
||||
saml_assertion_consumer_url_redirect:
|
||||
description:
|
||||
- SAML Redirect Binding URL for the client's assertion consumer service (login responses).
|
||||
|
||||
|
||||
saml_force_name_id_format:
|
||||
description:
|
||||
- For SAML clients, Boolean specifying whether to ignore requested NameID subject format and using the configured one instead.
|
||||
|
||||
saml_name_id_format:
|
||||
description:
|
||||
- For SAML clients, the NameID format to use (one of C(username), C(email), C(transient), or C(persistent))
|
||||
|
||||
saml_signature_canonicalization_method:
|
||||
description:
|
||||
- SAML signature canonicalization method. This is one of four values, namely
|
||||
C(http://www.w3.org/2001/10/xml-exc-c14n#) for EXCLUSIVE,
|
||||
C(http://www.w3.org/2001/10/xml-exc-c14n#WithComments) for EXCLUSIVE_WITH_COMMENTS,
|
||||
C(http://www.w3.org/TR/2001/REC-xml-c14n-20010315) for INCLUSIVE, and
|
||||
C(http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments) for INCLUSIVE_WITH_COMMENTS.
|
||||
|
||||
saml_single_logout_service_url_post:
|
||||
description:
|
||||
- SAML POST binding url for the client's single logout service.
|
||||
|
||||
saml_single_logout_service_url_redirect:
|
||||
description:
|
||||
- SAML redirect binding url for the client's single logout service.
|
||||
|
||||
user.info.response.signature.alg:
|
||||
description:
|
||||
- For OpenID-Connect clients, JWA algorithm for signed UserInfo-endpoint responses. One of C(RS256) or C(unsigned).
|
||||
|
||||
request.object.signature.alg:
|
||||
description:
|
||||
- For OpenID-Connect clients, JWA algorithm which the client needs to use when sending
|
||||
OIDC request object. One of C(any), C(none), C(RS256).
|
||||
|
||||
use.jwks.url:
|
||||
description:
|
||||
- For OpenID-Connect clients, boolean specifying whether to use a JWKS URL to obtain client
|
||||
public keys.
|
||||
|
||||
jwks.url:
|
||||
description:
|
||||
- For OpenID-Connect clients, URL where client keys in JWK are stored.
|
||||
|
||||
jwt.credential.certificate:
|
||||
description:
|
||||
- For OpenID-Connect clients, client certificate for validating JWT issued by
|
||||
client and signed by its key, base64-encoded.
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.keycloak
|
||||
|
||||
|
||||
author:
|
||||
- Eike Frost (@eikef)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create or update Keycloak client (minimal example)
|
||||
local_action:
|
||||
module: keycloak_client
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
client_id: test
|
||||
state: present
|
||||
|
||||
- name: Delete a Keycloak client
|
||||
local_action:
|
||||
module: keycloak_client
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
client_id: test
|
||||
state: absent
|
||||
|
||||
- name: Create or update a Keycloak client (with all the bells and whistles)
|
||||
local_action:
|
||||
module: keycloak_client
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
state: present
|
||||
realm: master
|
||||
client_id: test
|
||||
id: d8b127a3-31f6-44c8-a7e4-4ab9a3e78d95
|
||||
name: this_is_a_test
|
||||
description: Description of this wonderful client
|
||||
root_url: https://www.example.com/
|
||||
admin_url: https://www.example.com/admin_url
|
||||
base_url: basepath
|
||||
enabled: True
|
||||
client_authenticator_type: client-secret
|
||||
secret: REALLYWELLKEPTSECRET
|
||||
redirect_uris:
|
||||
- https://www.example.com/*
|
||||
- http://localhost:8888/
|
||||
web_origins:
|
||||
- https://www.example.com/*
|
||||
not_before: 1507825725
|
||||
bearer_only: False
|
||||
consent_required: False
|
||||
standard_flow_enabled: True
|
||||
implicit_flow_enabled: False
|
||||
direct_access_grants_enabled: False
|
||||
service_accounts_enabled: False
|
||||
authorization_services_enabled: False
|
||||
public_client: False
|
||||
frontchannel_logout: False
|
||||
protocol: openid-connect
|
||||
full_scope_allowed: false
|
||||
node_re_registration_timeout: -1
|
||||
client_template: test
|
||||
use_template_config: False
|
||||
use_template_scope: false
|
||||
use_template_mappers: no
|
||||
registered_nodes:
|
||||
node01.example.com: 1507828202
|
||||
registration_access_token: eyJWT_TOKEN
|
||||
surrogate_auth_required: false
|
||||
default_roles:
|
||||
- test01
|
||||
- test02
|
||||
protocol_mappers:
|
||||
- config:
|
||||
access.token.claim: True
|
||||
claim.name: "family_name"
|
||||
id.token.claim: True
|
||||
jsonType.label: String
|
||||
user.attribute: lastName
|
||||
userinfo.token.claim: True
|
||||
consentRequired: True
|
||||
consentText: "${familyName}"
|
||||
name: family name
|
||||
protocol: openid-connect
|
||||
protocolMapper: oidc-usermodel-property-mapper
|
||||
- config:
|
||||
attribute.name: Role
|
||||
attribute.nameformat: Basic
|
||||
single: false
|
||||
consentRequired: false
|
||||
name: role list
|
||||
protocol: saml
|
||||
protocolMapper: saml-role-list-mapper
|
||||
attributes:
|
||||
saml.authnstatement: True
|
||||
saml.client.signature: True
|
||||
saml.force.post.binding: True
|
||||
saml.server.signature: True
|
||||
saml.signature.algorithm: RSA_SHA256
|
||||
saml.signing.certificate: CERTIFICATEHERE
|
||||
saml.signing.private.key: PRIVATEKEYHERE
|
||||
saml_force_name_id_format: False
|
||||
saml_name_id_format: username
|
||||
saml_signature_canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#"
|
||||
user.info.response.signature.alg: RS256
|
||||
request.object.signature.alg: RS256
|
||||
use.jwks.url: true
|
||||
jwks.url: JWKS_URL_FOR_CLIENT_AUTH_JWT
|
||||
jwt.credential.certificate: JWT_CREDENTIAL_CERTIFICATE_FOR_CLIENT_AUTH
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Message as to what action was taken
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Client testclient has been updated"
|
||||
|
||||
proposed:
|
||||
description: client representation of proposed changes to client
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {
|
||||
clientId: "test"
|
||||
}
|
||||
existing:
|
||||
description: client representation of existing client (sample is truncated)
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {
|
||||
"adminUrl": "http://www.example.com/admin_url",
|
||||
"attributes": {
|
||||
"request.object.signature.alg": "RS256",
|
||||
}
|
||||
}
|
||||
end_state:
|
||||
description: client representation of client after module execution (sample is truncated)
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {
|
||||
"adminUrl": "http://www.example.com/admin_url",
|
||||
"attributes": {
|
||||
"request.object.signature.alg": "RS256",
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
|
||||
keycloak_argument_spec, get_token, KeycloakError
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def sanitize_cr(clientrep):
|
||||
""" Removes probably sensitive details from a client representation
|
||||
|
||||
:param clientrep: the clientrep dict to be sanitized
|
||||
:return: sanitized clientrep dict
|
||||
"""
|
||||
result = clientrep.copy()
|
||||
if 'secret' in result:
|
||||
result['secret'] = 'no_log'
|
||||
if 'attributes' in result:
|
||||
if 'saml.signing.private.key' in result['attributes']:
|
||||
result['attributes']['saml.signing.private.key'] = 'no_log'
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
protmapper_spec = dict(
|
||||
consentRequired=dict(type='bool'),
|
||||
consentText=dict(type='str'),
|
||||
id=dict(type='str'),
|
||||
name=dict(type='str'),
|
||||
protocol=dict(type='str', choices=['openid-connect', 'saml']),
|
||||
protocolMapper=dict(type='str'),
|
||||
config=dict(type='dict'),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
realm=dict(type='str', default='master'),
|
||||
|
||||
id=dict(type='str'),
|
||||
client_id=dict(type='str', aliases=['clientId']),
|
||||
name=dict(type='str'),
|
||||
description=dict(type='str'),
|
||||
root_url=dict(type='str', aliases=['rootUrl']),
|
||||
admin_url=dict(type='str', aliases=['adminUrl']),
|
||||
base_url=dict(type='str', aliases=['baseUrl']),
|
||||
surrogate_auth_required=dict(type='bool', aliases=['surrogateAuthRequired']),
|
||||
enabled=dict(type='bool'),
|
||||
client_authenticator_type=dict(type='str', choices=['client-secret', 'client-jwt'], aliases=['clientAuthenticatorType']),
|
||||
secret=dict(type='str', no_log=True),
|
||||
registration_access_token=dict(type='str', aliases=['registrationAccessToken']),
|
||||
default_roles=dict(type='list', aliases=['defaultRoles']),
|
||||
redirect_uris=dict(type='list', aliases=['redirectUris']),
|
||||
web_origins=dict(type='list', aliases=['webOrigins']),
|
||||
not_before=dict(type='int', aliases=['notBefore']),
|
||||
bearer_only=dict(type='bool', aliases=['bearerOnly']),
|
||||
consent_required=dict(type='bool', aliases=['consentRequired']),
|
||||
standard_flow_enabled=dict(type='bool', aliases=['standardFlowEnabled']),
|
||||
implicit_flow_enabled=dict(type='bool', aliases=['implicitFlowEnabled']),
|
||||
direct_access_grants_enabled=dict(type='bool', aliases=['directAccessGrantsEnabled']),
|
||||
service_accounts_enabled=dict(type='bool', aliases=['serviceAccountsEnabled']),
|
||||
authorization_services_enabled=dict(type='bool', aliases=['authorizationServicesEnabled']),
|
||||
public_client=dict(type='bool', aliases=['publicClient']),
|
||||
frontchannel_logout=dict(type='bool', aliases=['frontchannelLogout']),
|
||||
protocol=dict(type='str', choices=['openid-connect', 'saml']),
|
||||
attributes=dict(type='dict'),
|
||||
full_scope_allowed=dict(type='bool', aliases=['fullScopeAllowed']),
|
||||
node_re_registration_timeout=dict(type='int', aliases=['nodeReRegistrationTimeout']),
|
||||
registered_nodes=dict(type='dict', aliases=['registeredNodes']),
|
||||
client_template=dict(type='str', aliases=['clientTemplate']),
|
||||
use_template_config=dict(type='bool', aliases=['useTemplateConfig']),
|
||||
use_template_scope=dict(type='bool', aliases=['useTemplateScope']),
|
||||
use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']),
|
||||
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']),
|
||||
authorization_settings=dict(type='dict', aliases=['authorizationSettings']),
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=([['client_id', 'id']]))
|
||||
|
||||
result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(
|
||||
base_url=module.params.get('auth_keycloak_url'),
|
||||
validate_certs=module.params.get('validate_certs'),
|
||||
auth_realm=module.params.get('auth_realm'),
|
||||
client_id=module.params.get('auth_client_id'),
|
||||
auth_username=module.params.get('auth_username'),
|
||||
auth_password=module.params.get('auth_password'),
|
||||
client_secret=module.params.get('auth_client_secret'),
|
||||
)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get('realm')
|
||||
cid = module.params.get('id')
|
||||
state = module.params.get('state')
|
||||
|
||||
# convert module parameters to client representation parameters (if they belong in there)
|
||||
client_params = [x for x in module.params
|
||||
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and
|
||||
module.params.get(x) is not None]
|
||||
keycloak_argument_spec().keys()
|
||||
# See whether the client already exists in Keycloak
|
||||
if cid is None:
|
||||
before_client = kc.get_client_by_clientid(module.params.get('client_id'), realm=realm)
|
||||
if before_client is not None:
|
||||
cid = before_client['id']
|
||||
else:
|
||||
before_client = kc.get_client_by_id(cid, realm=realm)
|
||||
|
||||
if before_client is None:
|
||||
before_client = dict()
|
||||
|
||||
# Build a proposed changeset from parameters given to this module
|
||||
changeset = dict()
|
||||
|
||||
for client_param in client_params:
|
||||
new_param_value = module.params.get(client_param)
|
||||
|
||||
# some lists in the Keycloak API are sorted, some are not.
|
||||
if isinstance(new_param_value, list):
|
||||
if client_param in ['attributes']:
|
||||
try:
|
||||
new_param_value = sorted(new_param_value)
|
||||
except TypeError:
|
||||
pass
|
||||
# Unfortunately, the ansible argument spec checker introduces variables with null values when
|
||||
# they are not specified
|
||||
if client_param == 'protocol_mappers':
|
||||
new_param_value = [dict((k, v) for k, v in x.items() if x[k] is not None) for x in new_param_value]
|
||||
|
||||
changeset[camel(client_param)] = new_param_value
|
||||
|
||||
# Whether creating or updating a client, take the before-state and merge the changeset into it
|
||||
updated_client = before_client.copy()
|
||||
updated_client.update(changeset)
|
||||
|
||||
result['proposed'] = sanitize_cr(changeset)
|
||||
result['existing'] = sanitize_cr(before_client)
|
||||
|
||||
# If the client does not exist yet, before_client is still empty
|
||||
if before_client == dict():
|
||||
if state == 'absent':
|
||||
# do nothing and exit
|
||||
if module._diff:
|
||||
result['diff'] = dict(before='', after='')
|
||||
result['msg'] = 'Client does not exist, doing nothing.'
|
||||
module.exit_json(**result)
|
||||
|
||||
# create new client
|
||||
result['changed'] = True
|
||||
if 'clientId' not in updated_client:
|
||||
module.fail_json(msg='client_id needs to be specified when creating a new client')
|
||||
|
||||
if module._diff:
|
||||
result['diff'] = dict(before='', after=sanitize_cr(updated_client))
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.create_client(updated_client, realm=realm)
|
||||
after_client = kc.get_client_by_clientid(updated_client['clientId'], realm=realm)
|
||||
|
||||
result['end_state'] = sanitize_cr(after_client)
|
||||
|
||||
result['msg'] = 'Client %s has been created.' % updated_client['clientId']
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
if state == 'present':
|
||||
# update existing client
|
||||
result['changed'] = True
|
||||
if module.check_mode:
|
||||
# We can only compare the current client with the proposed updates we have
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=sanitize_cr(before_client),
|
||||
after=sanitize_cr(updated_client))
|
||||
result['changed'] = (before_client != updated_client)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.update_client(cid, updated_client, realm=realm)
|
||||
|
||||
after_client = kc.get_client_by_id(cid, realm=realm)
|
||||
if before_client == after_client:
|
||||
result['changed'] = False
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=sanitize_cr(before_client),
|
||||
after=sanitize_cr(after_client))
|
||||
result['end_state'] = sanitize_cr(after_client)
|
||||
|
||||
result['msg'] = 'Client %s has been updated.' % updated_client['clientId']
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
# Delete existing client
|
||||
result['changed'] = True
|
||||
if module._diff:
|
||||
result['diff']['before'] = sanitize_cr(before_client)
|
||||
result['diff']['after'] = ''
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.delete_client(cid, realm=realm)
|
||||
result['proposed'] = dict()
|
||||
result['end_state'] = dict()
|
||||
result['msg'] = 'Client %s has been deleted.' % before_client['clientId']
|
||||
module.exit_json(**result)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
420
plugins/modules/identity/keycloak/keycloak_clienttemplate.py
Normal file
420
plugins/modules/identity/keycloak/keycloak_clienttemplate.py
Normal file
|
@ -0,0 +1,420 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: keycloak_clienttemplate
|
||||
|
||||
short_description: Allows administration of Keycloak client templates via Keycloak API
|
||||
|
||||
|
||||
description:
|
||||
- This module allows the administration of Keycloak client templates via the Keycloak REST API. It
|
||||
requires access to the REST API via OpenID Connect; the user connecting and the client being
|
||||
used must have the requisite access rights. In a default Keycloak installation, admin-cli
|
||||
and an admin user would work, as would a separate client definition with the scope tailored
|
||||
to your needs and a user having the expected roles.
|
||||
|
||||
- The names of module options are snake_cased versions of the camelCase ones found in the
|
||||
Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html)
|
||||
|
||||
- The Keycloak API does not always enforce for only sensible settings to be used -- you can set
|
||||
SAML-specific settings on an OpenID Connect client for instance and vice versa. Be careful.
|
||||
If you do not specify a setting, usually a sensible default is chosen.
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the client template
|
||||
- On C(present), the client template will be created (or updated if it exists already).
|
||||
- On C(absent), the client template will be removed if it exists
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
|
||||
id:
|
||||
description:
|
||||
- Id of client template to be worked on. This is usually a UUID.
|
||||
|
||||
realm:
|
||||
description:
|
||||
- Realm this client template is found in.
|
||||
|
||||
name:
|
||||
description:
|
||||
- Name of the client template
|
||||
|
||||
description:
|
||||
description:
|
||||
- Description of the client template in Keycloak
|
||||
|
||||
protocol:
|
||||
description:
|
||||
- Type of client template (either C(openid-connect) or C(saml).
|
||||
choices: ['openid-connect', 'saml']
|
||||
|
||||
full_scope_allowed:
|
||||
description:
|
||||
- Is the "Full Scope Allowed" feature set for this client template or not.
|
||||
This is 'fullScopeAllowed' in the Keycloak REST API.
|
||||
type: bool
|
||||
|
||||
protocol_mappers:
|
||||
description:
|
||||
- a list of dicts defining protocol mappers for this client template.
|
||||
This is 'protocolMappers' in the Keycloak REST API.
|
||||
suboptions:
|
||||
consentRequired:
|
||||
description:
|
||||
- Specifies whether a user needs to provide consent to a client for this mapper to be active.
|
||||
|
||||
consentText:
|
||||
description:
|
||||
- The human-readable name of the consent the user is presented to accept.
|
||||
|
||||
id:
|
||||
description:
|
||||
- Usually a UUID specifying the internal ID of this protocol mapper instance.
|
||||
|
||||
name:
|
||||
description:
|
||||
- The name of this protocol mapper.
|
||||
|
||||
protocol:
|
||||
description:
|
||||
- is either 'openid-connect' or 'saml', this specifies for which protocol this protocol mapper
|
||||
is active.
|
||||
choices: ['openid-connect', 'saml']
|
||||
|
||||
protocolMapper:
|
||||
description:
|
||||
- The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is
|
||||
impossible to provide since this may be extended through SPIs by the user of Keycloak,
|
||||
by default Keycloak as of 3.4 ships with at least
|
||||
- C(docker-v2-allow-all-mapper)
|
||||
- C(oidc-address-mapper)
|
||||
- C(oidc-full-name-mapper)
|
||||
- C(oidc-group-membership-mapper)
|
||||
- C(oidc-hardcoded-claim-mapper)
|
||||
- C(oidc-hardcoded-role-mapper)
|
||||
- C(oidc-role-name-mapper)
|
||||
- C(oidc-script-based-protocol-mapper)
|
||||
- C(oidc-sha256-pairwise-sub-mapper)
|
||||
- C(oidc-usermodel-attribute-mapper)
|
||||
- C(oidc-usermodel-client-role-mapper)
|
||||
- C(oidc-usermodel-property-mapper)
|
||||
- C(oidc-usermodel-realm-role-mapper)
|
||||
- C(oidc-usersessionmodel-note-mapper)
|
||||
- C(saml-group-membership-mapper)
|
||||
- C(saml-hardcode-attribute-mapper)
|
||||
- C(saml-hardcode-role-mapper)
|
||||
- C(saml-role-list-mapper)
|
||||
- C(saml-role-name-mapper)
|
||||
- C(saml-user-attribute-mapper)
|
||||
- C(saml-user-property-mapper)
|
||||
- C(saml-user-session-note-mapper)
|
||||
- An exhaustive list of available mappers on your installation can be obtained on
|
||||
the admin console by going to Server Info -> Providers and looking under
|
||||
'protocol-mapper'.
|
||||
|
||||
config:
|
||||
description:
|
||||
- Dict specifying the configuration options for the protocol mapper; the
|
||||
contents differ depending on the value of I(protocolMapper) and are not documented
|
||||
other than by the source of the mappers and its parent class(es). An example is given
|
||||
below. It is easiest to obtain valid config values by dumping an already-existing
|
||||
protocol mapper configuration through check-mode in the "existing" field.
|
||||
|
||||
attributes:
|
||||
description:
|
||||
- A dict of further attributes for this client template. This can contain various
|
||||
configuration settings, though in the default installation of Keycloak as of 3.4, none
|
||||
are documented or known, so this is usually empty.
|
||||
|
||||
notes:
|
||||
- The Keycloak REST API defines further fields (namely I(bearerOnly), I(consentRequired), I(standardFlowEnabled),
|
||||
I(implicitFlowEnabled), I(directAccessGrantsEnabled), I(serviceAccountsEnabled), I(publicClient), and
|
||||
I(frontchannelLogout)) which, while available with keycloak_client, do not have any effect on
|
||||
Keycloak client-templates and are discarded if supplied with an API request changing client-templates. As such,
|
||||
they are not available through this module.
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.keycloak
|
||||
|
||||
|
||||
author:
|
||||
- Eike Frost (@eikef)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create or update Keycloak client template (minimal)
|
||||
local_action:
|
||||
module: keycloak_clienttemplate
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: master
|
||||
name: this_is_a_test
|
||||
|
||||
- name: delete Keycloak client template
|
||||
local_action:
|
||||
module: keycloak_clienttemplate
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: master
|
||||
state: absent
|
||||
name: test01
|
||||
|
||||
- name: Create or update Keycloak client template (with a protocol mapper)
|
||||
local_action:
|
||||
module: keycloak_clienttemplate
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: master
|
||||
name: this_is_a_test
|
||||
protocol_mappers:
|
||||
- config:
|
||||
access.token.claim: True
|
||||
claim.name: "family_name"
|
||||
id.token.claim: True
|
||||
jsonType.label: String
|
||||
user.attribute: lastName
|
||||
userinfo.token.claim: True
|
||||
consentRequired: True
|
||||
consentText: "${familyName}"
|
||||
name: family name
|
||||
protocol: openid-connect
|
||||
protocolMapper: oidc-usermodel-property-mapper
|
||||
full_scope_allowed: false
|
||||
id: bce6f5e9-d7d3-4955-817e-c5b7f8d65b3f
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Message as to what action was taken
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Client template testclient has been updated"
|
||||
|
||||
proposed:
|
||||
description: client template representation of proposed changes to client template
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {
|
||||
name: "test01"
|
||||
}
|
||||
existing:
|
||||
description: client template representation of existing client template (sample is truncated)
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {
|
||||
"description": "test01",
|
||||
"fullScopeAllowed": false,
|
||||
"id": "9c3712ab-decd-481e-954f-76da7b006e5f",
|
||||
"name": "test01",
|
||||
"protocol": "saml"
|
||||
}
|
||||
end_state:
|
||||
description: client template representation of client template after module execution (sample is truncated)
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {
|
||||
"description": "test01",
|
||||
"fullScopeAllowed": false,
|
||||
"id": "9c3712ab-decd-481e-954f-76da7b006e5f",
|
||||
"name": "test01",
|
||||
"protocol": "saml"
|
||||
}
|
||||
'''
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
|
||||
keycloak_argument_spec, get_token, KeycloakError
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
protmapper_spec = dict(
|
||||
consentRequired=dict(type='bool'),
|
||||
consentText=dict(type='str'),
|
||||
id=dict(type='str'),
|
||||
name=dict(type='str'),
|
||||
protocol=dict(type='str', choices=['openid-connect', 'saml']),
|
||||
protocolMapper=dict(type='str'),
|
||||
config=dict(type='dict'),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(type='str', default='master'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
|
||||
id=dict(type='str'),
|
||||
name=dict(type='str'),
|
||||
description=dict(type='str'),
|
||||
protocol=dict(type='str', choices=['openid-connect', 'saml']),
|
||||
attributes=dict(type='dict'),
|
||||
full_scope_allowed=dict(type='bool'),
|
||||
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec),
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=([['id', 'name']]))
|
||||
|
||||
result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(
|
||||
base_url=module.params.get('auth_keycloak_url'),
|
||||
validate_certs=module.params.get('validate_certs'),
|
||||
auth_realm=module.params.get('auth_realm'),
|
||||
client_id=module.params.get('auth_client_id'),
|
||||
auth_username=module.params.get('auth_username'),
|
||||
auth_password=module.params.get('auth_password'),
|
||||
client_secret=module.params.get('auth_client_secret'),
|
||||
)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get('realm')
|
||||
state = module.params.get('state')
|
||||
cid = module.params.get('id')
|
||||
|
||||
# convert module parameters to client representation parameters (if they belong in there)
|
||||
clientt_params = [x for x in module.params
|
||||
if x not in ['state', 'auth_keycloak_url', 'auth_client_id', 'auth_realm',
|
||||
'auth_client_secret', 'auth_username', 'auth_password',
|
||||
'validate_certs', 'realm'] and module.params.get(x) is not None]
|
||||
|
||||
# See whether the client template already exists in Keycloak
|
||||
if cid is None:
|
||||
before_clientt = kc.get_client_template_by_name(module.params.get('name'), realm=realm)
|
||||
if before_clientt is not None:
|
||||
cid = before_clientt['id']
|
||||
else:
|
||||
before_clientt = kc.get_client_template_by_id(cid, realm=realm)
|
||||
|
||||
if before_clientt is None:
|
||||
before_clientt = dict()
|
||||
|
||||
result['existing'] = before_clientt
|
||||
|
||||
# Build a proposed changeset from parameters given to this module
|
||||
changeset = dict()
|
||||
|
||||
for clientt_param in clientt_params:
|
||||
# lists in the Keycloak API are sorted
|
||||
new_param_value = module.params.get(clientt_param)
|
||||
if isinstance(new_param_value, list):
|
||||
try:
|
||||
new_param_value = sorted(new_param_value)
|
||||
except TypeError:
|
||||
pass
|
||||
changeset[camel(clientt_param)] = new_param_value
|
||||
|
||||
# Whether creating or updating a client, take the before-state and merge the changeset into it
|
||||
updated_clientt = before_clientt.copy()
|
||||
updated_clientt.update(changeset)
|
||||
|
||||
result['proposed'] = changeset
|
||||
|
||||
# If the client template does not exist yet, before_client is still empty
|
||||
if before_clientt == dict():
|
||||
if state == 'absent':
|
||||
# do nothing and exit
|
||||
if module._diff:
|
||||
result['diff'] = dict(before='', after='')
|
||||
result['msg'] = 'Client template does not exist, doing nothing.'
|
||||
module.exit_json(**result)
|
||||
|
||||
# create new client template
|
||||
result['changed'] = True
|
||||
if 'name' not in updated_clientt:
|
||||
module.fail_json(msg='name needs to be specified when creating a new client')
|
||||
|
||||
if module._diff:
|
||||
result['diff'] = dict(before='', after=updated_clientt)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.create_client_template(updated_clientt, realm=realm)
|
||||
after_clientt = kc.get_client_template_by_name(updated_clientt['name'], realm=realm)
|
||||
|
||||
result['end_state'] = after_clientt
|
||||
|
||||
result['msg'] = 'Client template %s has been created.' % updated_clientt['name']
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
if state == 'present':
|
||||
# update existing client template
|
||||
result['changed'] = True
|
||||
if module.check_mode:
|
||||
# We can only compare the current client template with the proposed updates we have
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=before_clientt,
|
||||
after=updated_clientt)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.update_client_template(cid, updated_clientt, realm=realm)
|
||||
|
||||
after_clientt = kc.get_client_template_by_id(cid, realm=realm)
|
||||
if before_clientt == after_clientt:
|
||||
result['changed'] = False
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=before_clientt,
|
||||
after=after_clientt)
|
||||
result['end_state'] = after_clientt
|
||||
|
||||
result['msg'] = 'Client template %s has been updated.' % updated_clientt['name']
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
# Delete existing client
|
||||
result['changed'] = True
|
||||
if module._diff:
|
||||
result['diff']['before'] = before_clientt
|
||||
result['diff']['after'] = ''
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.delete_client_template(cid, realm=realm)
|
||||
result['proposed'] = dict()
|
||||
result['end_state'] = dict()
|
||||
result['msg'] = 'Client template %s has been deleted.' % before_clientt['name']
|
||||
module.exit_json(**result)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
370
plugins/modules/identity/keycloak/keycloak_group.py
Normal file
370
plugins/modules/identity/keycloak/keycloak_group.py
Normal file
|
@ -0,0 +1,370 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019, Adam Goossens <adam.goossens@gmail.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': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: keycloak_group
|
||||
|
||||
short_description: Allows administration of Keycloak groups via Keycloak API
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak groups via the Keycloak REST API.
|
||||
It requires access to the REST API via OpenID Connect; the user connecting and the client being
|
||||
used must have the requisite access rights. In a default Keycloak installation, admin-cli
|
||||
and an admin user would work, as would a separate client definition with the scope tailored
|
||||
to your needs and a user having the expected roles.
|
||||
|
||||
- The names of module options are snake_cased versions of the camelCase ones found in the
|
||||
Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html).
|
||||
|
||||
- Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and will
|
||||
be returned that way by this module. You may pass single values for attributes when calling the module,
|
||||
and this will be translated into a list suitable for the API.
|
||||
|
||||
- When updating a group, where possible provide the group ID to the module. This removes a lookup
|
||||
to the API to translate the name into the group ID.
|
||||
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the group.
|
||||
- On C(present), the group will be created if it does not yet exist, or updated with the parameters you provide.
|
||||
- On C(absent), the group will be removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- Name of the group.
|
||||
- This parameter is required only when creating or updating the group.
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- They Keycloak realm under which this group resides.
|
||||
default: 'master'
|
||||
|
||||
id:
|
||||
type: str
|
||||
description:
|
||||
- The unique identifier for this group.
|
||||
- This parameter is not required for updating or deleting a group but
|
||||
providing it will reduce the number of API calls required.
|
||||
|
||||
attributes:
|
||||
type: dict
|
||||
description:
|
||||
- A dict of key/value pairs to set as custom attributes for the group.
|
||||
- Values may be single values (e.g. a string) or a list of strings.
|
||||
|
||||
notes:
|
||||
- Presently, the I(realmRoles), I(clientRoles) and I(access) attributes returned by the Keycloak API
|
||||
are read-only for groups. This limitation will be removed in a later version of this module.
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.keycloak
|
||||
|
||||
|
||||
author:
|
||||
- Adam Goossens (@adamgoossens)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create a Keycloak group
|
||||
keycloak_group:
|
||||
name: my-new-kc-group
|
||||
realm: MyCustomRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete a keycloak group
|
||||
keycloak_group:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
state: absent
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete a Keycloak group based on name
|
||||
keycloak_group:
|
||||
name: my-group-for-deletion
|
||||
state: absent
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Update the name of a Keycloak group
|
||||
keycloak_group:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
name: an-updated-kc-group-name
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a keycloak group with some custom attributes
|
||||
keycloak_group:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com/auth
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
name: my-new_group
|
||||
attributes:
|
||||
attrib1: value1
|
||||
attrib2: value2
|
||||
attrib3:
|
||||
- with
|
||||
- numerous
|
||||
- individual
|
||||
- list
|
||||
- items
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
group:
|
||||
description: Group representation of the group after module execution (sample is truncated).
|
||||
returned: always
|
||||
type: complex
|
||||
contains:
|
||||
id:
|
||||
description: GUID that identifies the group
|
||||
type: str
|
||||
returned: always
|
||||
sample: 23f38145-3195-462c-97e7-97041ccea73e
|
||||
name:
|
||||
description: Name of the group
|
||||
type: str
|
||||
returned: always
|
||||
sample: grp-test-123
|
||||
attributes:
|
||||
description: Attributes applied to this group
|
||||
type: dict
|
||||
returned: always
|
||||
sample:
|
||||
attr1: ["val1", "val2", "val3"]
|
||||
path:
|
||||
description: URI path to the group
|
||||
type: str
|
||||
returned: always
|
||||
sample: /grp-test-123
|
||||
realmRoles:
|
||||
description: An array of the realm-level roles granted to this group
|
||||
type: list
|
||||
returned: always
|
||||
sample: []
|
||||
subGroups:
|
||||
description: A list of groups that are children of this group. These groups will have the same parameters as
|
||||
documented here.
|
||||
type: list
|
||||
returned: always
|
||||
clientRoles:
|
||||
description: A list of client-level roles granted to this group
|
||||
type: list
|
||||
returned: always
|
||||
sample: []
|
||||
access:
|
||||
description: A dict describing the accesses you have to this group based on the credentials used.
|
||||
type: dict
|
||||
returned: always
|
||||
sample:
|
||||
manage: true
|
||||
manageMembership: true
|
||||
view: true
|
||||
'''
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
|
||||
keycloak_argument_spec, get_token, KeycloakError
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
meta_args = dict(
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
realm=dict(default='master'),
|
||||
id=dict(type='str'),
|
||||
name=dict(type='str'),
|
||||
attributes=dict(type='dict')
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=([['id', 'name']]))
|
||||
|
||||
result = dict(changed=False, msg='', diff={}, group='')
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(
|
||||
base_url=module.params.get('auth_keycloak_url'),
|
||||
validate_certs=module.params.get('validate_certs'),
|
||||
auth_realm=module.params.get('auth_realm'),
|
||||
client_id=module.params.get('auth_client_id'),
|
||||
auth_username=module.params.get('auth_username'),
|
||||
auth_password=module.params.get('auth_password'),
|
||||
client_secret=module.params.get('auth_client_secret'),
|
||||
)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get('realm')
|
||||
state = module.params.get('state')
|
||||
gid = module.params.get('id')
|
||||
name = module.params.get('name')
|
||||
attributes = module.params.get('attributes')
|
||||
|
||||
before_group = None # current state of the group, for merging.
|
||||
|
||||
# does the group already exist?
|
||||
if gid is None:
|
||||
before_group = kc.get_group_by_name(name, realm=realm)
|
||||
else:
|
||||
before_group = kc.get_group_by_groupid(gid, realm=realm)
|
||||
|
||||
before_group = {} if before_group is None else before_group
|
||||
|
||||
# attributes in Keycloak have their values returned as lists
|
||||
# via the API. attributes is a dict, so we'll transparently convert
|
||||
# the values to lists.
|
||||
if attributes is not None:
|
||||
for key, val in module.params['attributes'].items():
|
||||
module.params['attributes'][key] = [val] if not isinstance(val, list) else val
|
||||
|
||||
group_params = [x for x in module.params
|
||||
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and
|
||||
module.params.get(x) is not None]
|
||||
|
||||
# build a changeset
|
||||
changeset = {}
|
||||
for param in group_params:
|
||||
new_param_value = module.params.get(param)
|
||||
old_value = before_group[param] if param in before_group else None
|
||||
if new_param_value != old_value:
|
||||
changeset[camel(param)] = new_param_value
|
||||
|
||||
# prepare the new group
|
||||
updated_group = before_group.copy()
|
||||
updated_group.update(changeset)
|
||||
|
||||
# if before_group is none, the group doesn't exist.
|
||||
if before_group == {}:
|
||||
if state == 'absent':
|
||||
# nothing to do.
|
||||
if module._diff:
|
||||
result['diff'] = dict(before='', after='')
|
||||
result['msg'] = 'Group does not exist; doing nothing.'
|
||||
result['group'] = dict()
|
||||
module.exit_json(**result)
|
||||
|
||||
# for 'present', create a new group.
|
||||
result['changed'] = True
|
||||
if name is None:
|
||||
module.fail_json(msg='name must be specified when creating a new group')
|
||||
|
||||
if module._diff:
|
||||
result['diff'] = dict(before='', after=updated_group)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# do it for real!
|
||||
kc.create_group(updated_group, realm=realm)
|
||||
after_group = kc.get_group_by_name(name, realm)
|
||||
|
||||
result['group'] = after_group
|
||||
result['msg'] = 'Group {name} has been created with ID {id}'.format(name=after_group['name'],
|
||||
id=after_group['id'])
|
||||
|
||||
else:
|
||||
if state == 'present':
|
||||
# no changes
|
||||
if updated_group == before_group:
|
||||
result['changed'] = False
|
||||
result['group'] = updated_group
|
||||
result['msg'] = "No changes required to group {name}.".format(name=before_group['name'])
|
||||
module.exit_json(**result)
|
||||
|
||||
# update the existing group
|
||||
result['changed'] = True
|
||||
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=before_group, after=updated_group)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# do the update
|
||||
kc.update_group(updated_group, realm=realm)
|
||||
|
||||
after_group = kc.get_group_by_groupid(updated_group['id'], realm=realm)
|
||||
|
||||
result['group'] = after_group
|
||||
result['msg'] = "Group {id} has been updated".format(id=after_group['id'])
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == 'absent':
|
||||
result['group'] = dict()
|
||||
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=before_group, after='')
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# delete for real
|
||||
gid = before_group['id']
|
||||
kc.delete_group(groupid=gid, realm=realm)
|
||||
|
||||
result['changed'] = True
|
||||
result['msg'] = "Group {name} has been deleted".format(name=before_group['name'])
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
1
plugins/modules/identity/onepassword_facts.py
Symbolic link
1
plugins/modules/identity/onepassword_facts.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
onepassword_info.py
|
395
plugins/modules/identity/onepassword_info.py
Normal file
395
plugins/modules/identity/onepassword_info.py
Normal file
|
@ -0,0 +1,395 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2018, Ryan Conway (@rylon)
|
||||
# (c) 2018, Scott Buchanan <sbuchanan@ri.pn> (onepassword.py used as starting point)
|
||||
# (c) 2018, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
module: onepassword_info
|
||||
author:
|
||||
- Ryan Conway (@Rylon)
|
||||
requirements:
|
||||
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/)
|
||||
notes:
|
||||
- Tested with C(op) version 0.5.5
|
||||
- "Based on the C(onepassword) lookup plugin by Scott Buchanan <sbuchanan@ri.pn>."
|
||||
- When this module is called with the deprecated C(onepassword_facts) name, potentially sensitive data
|
||||
from 1Password is returned as Ansible facts. Facts are subject to caching if enabled, which means this
|
||||
data could be stored in clear text on disk or in a database.
|
||||
short_description: Gather items from 1Password
|
||||
description:
|
||||
- M(onepassword_info) wraps the C(op) command line utility to fetch data about one or more 1Password items.
|
||||
- A fatal error occurs if any of the items being searched for can not be found.
|
||||
- Recommend using with the C(no_log) option to avoid logging the values of the secrets being retrieved.
|
||||
- This module was called C(onepassword_facts) before Ansible 2.9, returning C(ansible_facts).
|
||||
Note that the M(onepassword_info) module no longer returns C(ansible_facts)!
|
||||
You must now use the C(register) option to use the facts in other tasks.
|
||||
options:
|
||||
search_terms:
|
||||
type: list
|
||||
description:
|
||||
- A list of one or more search terms.
|
||||
- Each search term can either be a simple string or it can be a dictionary for more control.
|
||||
- When passing a simple string, I(field) is assumed to be C(password).
|
||||
- When passing a dictionary, the following fields are available.
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- The name of the 1Password item to search for (required).
|
||||
field:
|
||||
type: str
|
||||
description:
|
||||
- The name of the field to search for within this item (optional, defaults to "password" (or "document" if the item has an attachment).
|
||||
section:
|
||||
type: str
|
||||
description:
|
||||
- The name of a section within this item containing the specified field (optional, will search all sections if not specified).
|
||||
vault:
|
||||
type: str
|
||||
description:
|
||||
- The name of the particular 1Password vault to search, useful if your 1Password user has access to multiple vaults (optional).
|
||||
required: True
|
||||
auto_login:
|
||||
type: dict
|
||||
description:
|
||||
- A dictionary containing authentication details. If this is set, M(onepassword_info) will attempt to sign in to 1Password automatically.
|
||||
- Without this option, you must have already logged in via the 1Password CLI before running Ansible.
|
||||
- It is B(highly) recommended to store 1Password credentials in an Ansible Vault. Ensure that the key used to encrypt
|
||||
the Ansible Vault is equal to or greater in strength than the 1Password master password.
|
||||
suboptions:
|
||||
subdomain:
|
||||
type: str
|
||||
description:
|
||||
- 1Password subdomain name (<subdomain>.1password.com).
|
||||
- If this is not specified, the most recent subdomain will be used.
|
||||
username:
|
||||
type: str
|
||||
description:
|
||||
- 1Password username.
|
||||
- Only required for initial sign in.
|
||||
master_password:
|
||||
type: str
|
||||
description:
|
||||
- The master password for your subdomain.
|
||||
- This is always required when specifying C(auto_login).
|
||||
required: True
|
||||
secret_key:
|
||||
type: str
|
||||
description:
|
||||
- The secret key for your subdomain.
|
||||
- Only required for initial sign in.
|
||||
default: {}
|
||||
required: False
|
||||
cli_path:
|
||||
type: path
|
||||
description: Used to specify the exact path to the C(op) command line interface
|
||||
required: False
|
||||
default: 'op'
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Gather secrets from 1Password, assuming there is a 'password' field:
|
||||
- name: Get a password
|
||||
onepassword_info:
|
||||
search_terms: My 1Password item
|
||||
delegate_to: localhost
|
||||
register: my_1password_item
|
||||
no_log: true # Don't want to log the secrets to the console!
|
||||
|
||||
# Gather secrets from 1Password, with more advanced search terms:
|
||||
- name: Get a password
|
||||
onepassword_info:
|
||||
search_terms:
|
||||
- name: My 1Password item
|
||||
field: Custom field name # optional, defaults to 'password'
|
||||
section: Custom section name # optional, defaults to 'None'
|
||||
vault: Name of the vault # optional, only necessary if there is more than 1 Vault available
|
||||
delegate_to: localhost
|
||||
register: my_1password_item
|
||||
no_log: True # Don't want to log the secrets to the console!
|
||||
|
||||
# Gather secrets combining simple and advanced search terms to retrieve two items, one of which we fetch two
|
||||
# fields. In the first 'password' is fetched, as a field name is not specified (default behaviour) and in the
|
||||
# second, 'Custom field name' is fetched, as that is specified explicitly.
|
||||
- name: Get a password
|
||||
onepassword_info:
|
||||
search_terms:
|
||||
- My 1Password item # 'name' is optional when passing a simple string...
|
||||
- name: My Other 1Password item # ...but it can also be set for consistency
|
||||
- name: My 1Password item
|
||||
field: Custom field name # optional, defaults to 'password'
|
||||
section: Custom section name # optional, defaults to 'None'
|
||||
vault: Name of the vault # optional, only necessary if there is more than 1 Vault available
|
||||
- name: A 1Password item with document attachment
|
||||
delegate_to: localhost
|
||||
register: my_1password_item
|
||||
no_log: true # Don't want to log the secrets to the console!
|
||||
|
||||
- name: Debug a password (for example)
|
||||
debug:
|
||||
msg: "{{ my_1password_item['onepassword']['My 1Password item'] }}"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
---
|
||||
# One or more dictionaries for each matching item from 1Password, along with the appropriate fields.
|
||||
# This shows the response you would expect to receive from the third example documented above.
|
||||
onepassword:
|
||||
description: Dictionary of each 1password item matching the given search terms, shows what would be returned from the third example above.
|
||||
returned: success
|
||||
type: dict
|
||||
sample:
|
||||
"My 1Password item":
|
||||
password: the value of this field
|
||||
Custom field name: the value of this field
|
||||
"My Other 1Password item":
|
||||
password: the value of this field
|
||||
"A 1Password item with document attachment":
|
||||
document: the contents of the document attached to this item
|
||||
'''
|
||||
|
||||
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_native
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
class AnsibleModuleError(Exception):
|
||||
def __init__(self, results):
|
||||
self.results = results
|
||||
|
||||
def __repr__(self):
|
||||
return self.results
|
||||
|
||||
|
||||
class OnePasswordInfo(object):
|
||||
|
||||
def __init__(self):
|
||||
self.cli_path = module.params.get('cli_path')
|
||||
self.config_file_path = '~/.op/config'
|
||||
self.auto_login = module.params.get('auto_login')
|
||||
self.logged_in = False
|
||||
self.token = None
|
||||
|
||||
terms = module.params.get('search_terms')
|
||||
self.terms = self.parse_search_terms(terms)
|
||||
|
||||
def _run(self, args, expected_rc=0, command_input=None, ignore_errors=False):
|
||||
if self.token:
|
||||
# Adds the session token to all commands if we're logged in.
|
||||
args += [to_bytes('--session=') + self.token]
|
||||
|
||||
command = [self.cli_path] + args
|
||||
p = Popen(command, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
||||
out, err = p.communicate(input=command_input)
|
||||
rc = p.wait()
|
||||
if not ignore_errors and rc != expected_rc:
|
||||
raise AnsibleModuleError(to_native(err))
|
||||
return rc, out, err
|
||||
|
||||
def _parse_field(self, data_json, item_id, field_name, section_title=None):
|
||||
data = json.loads(data_json)
|
||||
|
||||
if ('documentAttributes' in data['details']):
|
||||
# This is actually a document, let's fetch the document data instead!
|
||||
document = self._run(["get", "document", data['overview']['title']])
|
||||
return {'document': document[1].strip()}
|
||||
|
||||
else:
|
||||
# This is not a document, let's try to find the requested field
|
||||
|
||||
# Some types of 1Password items have a 'password' field directly alongside the 'fields' attribute,
|
||||
# not inside it, so we need to check there first.
|
||||
if (field_name in data['details']):
|
||||
return {field_name: data['details'][field_name]}
|
||||
|
||||
# Otherwise we continue looking inside the 'fields' attribute for the specified field.
|
||||
else:
|
||||
if section_title is None:
|
||||
for field_data in data['details'].get('fields', []):
|
||||
if field_data.get('name', '').lower() == field_name.lower():
|
||||
return {field_name: field_data.get('value', '')}
|
||||
|
||||
# Not found it yet, so now lets see if there are any sections defined
|
||||
# and search through those for the field. If a section was given, we skip
|
||||
# any non-matching sections, otherwise we search them all until we find the field.
|
||||
for section_data in data['details'].get('sections', []):
|
||||
if section_title is not None and section_title.lower() != section_data['title'].lower():
|
||||
continue
|
||||
for field_data in section_data.get('fields', []):
|
||||
if field_data.get('t', '').lower() == field_name.lower():
|
||||
return {field_name: field_data.get('v', '')}
|
||||
|
||||
# We will get here if the field could not be found in any section and the item wasn't a document to be downloaded.
|
||||
optional_section_title = '' if section_title is None else " in the section '%s'" % section_title
|
||||
module.fail_json(msg="Unable to find an item in 1Password named '%s' with the field '%s'%s." % (item_id, field_name, optional_section_title))
|
||||
|
||||
def parse_search_terms(self, terms):
|
||||
processed_terms = []
|
||||
|
||||
for term in terms:
|
||||
if not isinstance(term, dict):
|
||||
term = {'name': term}
|
||||
|
||||
if 'name' not in term:
|
||||
module.fail_json(msg="Missing required 'name' field from search term, got: '%s'" % to_native(term))
|
||||
|
||||
term['field'] = term.get('field', 'password')
|
||||
term['section'] = term.get('section', None)
|
||||
term['vault'] = term.get('vault', None)
|
||||
|
||||
processed_terms.append(term)
|
||||
|
||||
return processed_terms
|
||||
|
||||
def get_raw(self, item_id, vault=None):
|
||||
try:
|
||||
args = ["get", "item", item_id]
|
||||
if vault is not None:
|
||||
args += ['--vault={0}'.format(vault)]
|
||||
rc, output, dummy = self._run(args)
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
if re.search(".*not found.*", to_native(e)):
|
||||
module.fail_json(msg="Unable to find an item in 1Password named '%s'." % item_id)
|
||||
else:
|
||||
module.fail_json(msg="Unexpected error attempting to find an item in 1Password named '%s': %s" % (item_id, to_native(e)))
|
||||
|
||||
def get_field(self, item_id, field, section=None, vault=None):
|
||||
output = self.get_raw(item_id, vault)
|
||||
return self._parse_field(output, item_id, field, section) if output != '' else ''
|
||||
|
||||
def full_login(self):
|
||||
if self.auto_login is not None:
|
||||
if None in [self.auto_login.get('subdomain'), self.auto_login.get('username'),
|
||||
self.auto_login.get('secret_key'), self.auto_login.get('master_password')]:
|
||||
module.fail_json(msg='Unable to perform initial sign in to 1Password. '
|
||||
'subdomain, username, secret_key, and master_password are required to perform initial sign in.')
|
||||
|
||||
args = [
|
||||
'signin',
|
||||
'{0}.1password.com'.format(self.auto_login['subdomain']),
|
||||
to_bytes(self.auto_login['username']),
|
||||
to_bytes(self.auto_login['secret_key']),
|
||||
'--output=raw',
|
||||
]
|
||||
|
||||
try:
|
||||
rc, out, err = self._run(args, command_input=to_bytes(self.auto_login['master_password']))
|
||||
self.token = out.strip()
|
||||
except AnsibleModuleError as e:
|
||||
module.fail_json(msg="Failed to perform initial sign in to 1Password: %s" % to_native(e))
|
||||
else:
|
||||
module.fail_json(msg="Unable to perform an initial sign in to 1Password. Please run '%s sigin' "
|
||||
"or define credentials in 'auto_login'. See the module documentation for details." % self.cli_path)
|
||||
|
||||
def get_token(self):
|
||||
# If the config file exists, assume an initial signin has taken place and try basic sign in
|
||||
if os.path.isfile(self.config_file_path):
|
||||
|
||||
if self.auto_login is not None:
|
||||
|
||||
# Since we are not currently signed in, master_password is required at a minimum
|
||||
if not self.auto_login.get('master_password'):
|
||||
module.fail_json(msg="Unable to sign in to 1Password. 'auto_login.master_password' is required.")
|
||||
|
||||
# Try signing in using the master_password and a subdomain if one is provided
|
||||
try:
|
||||
args = ['signin', '--output=raw']
|
||||
|
||||
if self.auto_login.get('subdomain'):
|
||||
args = ['signin', self.auto_login['subdomain'], '--output=raw']
|
||||
|
||||
rc, out, err = self._run(args, command_input=to_bytes(self.auto_login['master_password']))
|
||||
self.token = out.strip()
|
||||
|
||||
except AnsibleModuleError:
|
||||
self.full_login()
|
||||
|
||||
else:
|
||||
self.full_login()
|
||||
|
||||
else:
|
||||
# Attempt a full sign in since there appears to be no existing sign in
|
||||
self.full_login()
|
||||
|
||||
def assert_logged_in(self):
|
||||
try:
|
||||
rc, out, err = self._run(['get', 'account'], ignore_errors=True)
|
||||
if rc == 0:
|
||||
self.logged_in = True
|
||||
if not self.logged_in:
|
||||
self.get_token()
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
module.fail_json(msg="1Password CLI tool '%s' not installed in path on control machine" % self.cli_path)
|
||||
raise e
|
||||
|
||||
def run(self):
|
||||
result = {}
|
||||
|
||||
self.assert_logged_in()
|
||||
|
||||
for term in self.terms:
|
||||
value = self.get_field(term['name'], term['field'], term['section'], term['vault'])
|
||||
|
||||
if term['name'] in result:
|
||||
# If we already have a result for this key, we have to append this result dictionary
|
||||
# to the existing one. This is only applicable when there is a single item
|
||||
# in 1Password which has two different fields, and we want to retrieve both of them.
|
||||
result[term['name']].update(value)
|
||||
else:
|
||||
# If this is the first result for this key, simply set it.
|
||||
result[term['name']] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
global module
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
cli_path=dict(type='path', default='op'),
|
||||
auto_login=dict(type='dict', options=dict(
|
||||
subdomain=dict(type='str'),
|
||||
username=dict(type='str'),
|
||||
master_password=dict(required=True, type='str', no_log=True),
|
||||
secret_key=dict(type='str', no_log=True),
|
||||
), default=None),
|
||||
search_terms=dict(required=True, type='list')
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
results = {'onepassword': OnePasswordInfo().run()}
|
||||
|
||||
if module._name == 'onepassword_facts':
|
||||
module.deprecate("The 'onepassword_facts' module has been renamed to 'onepassword_info'. "
|
||||
"When called with the new name it no longer returns 'ansible_facts'", version='2.13')
|
||||
module.exit_json(changed=False, ansible_facts=results)
|
||||
else:
|
||||
module.exit_json(changed=False, **results)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
203
plugins/modules/identity/opendj/opendj_backendprop.py
Normal file
203
plugins/modules/identity/opendj/opendj_backendprop.py
Normal file
|
@ -0,0 +1,203 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2016, Werner Dijkerman (ikben@werner-dijkerman.nl)
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: opendj_backendprop
|
||||
short_description: Will update the backend configuration of OpenDJ via the dsconfig set-backend-prop command.
|
||||
description:
|
||||
- This module will update settings for OpenDJ with the command set-backend-prop.
|
||||
- It will check first via de get-backend-prop if configuration needs to be applied.
|
||||
author:
|
||||
- Werner Dijkerman (@dj-wasabi)
|
||||
options:
|
||||
opendj_bindir:
|
||||
description:
|
||||
- The path to the bin directory of OpenDJ.
|
||||
required: false
|
||||
default: /opt/opendj/bin
|
||||
hostname:
|
||||
description:
|
||||
- The hostname of the OpenDJ server.
|
||||
required: true
|
||||
port:
|
||||
description:
|
||||
- The Admin port on which the OpenDJ instance is available.
|
||||
required: true
|
||||
username:
|
||||
description:
|
||||
- The username to connect to.
|
||||
required: false
|
||||
default: cn=Directory Manager
|
||||
password:
|
||||
description:
|
||||
- The password for the cn=Directory Manager user.
|
||||
- Either password or passwordfile is needed.
|
||||
required: false
|
||||
passwordfile:
|
||||
description:
|
||||
- Location to the password file which holds the password for the cn=Directory Manager user.
|
||||
- Either password or passwordfile is needed.
|
||||
required: false
|
||||
backend:
|
||||
description:
|
||||
- The name of the backend on which the property needs to be updated.
|
||||
required: true
|
||||
name:
|
||||
description:
|
||||
- The configuration setting to update.
|
||||
required: true
|
||||
value:
|
||||
description:
|
||||
- The value for the configuration item.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- If configuration needs to be added/updated
|
||||
required: false
|
||||
default: "present"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: "Add or update OpenDJ backend properties"
|
||||
action: opendj_backendprop
|
||||
hostname=localhost
|
||||
port=4444
|
||||
username="cn=Directory Manager"
|
||||
password=password
|
||||
backend=userRoot
|
||||
name=index-entry-limit
|
||||
value=5000
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
class BackendProp(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
|
||||
def get_property(self, opendj_bindir, hostname, port, username, password_method, backend_name):
|
||||
my_command = [
|
||||
opendj_bindir + '/dsconfig',
|
||||
'get-backend-prop',
|
||||
'-h', hostname,
|
||||
'--port', str(port),
|
||||
'--bindDN', username,
|
||||
'--backend-name', backend_name,
|
||||
'-n', '-X', '-s'
|
||||
] + password_method
|
||||
rc, stdout, stderr = self._module.run_command(my_command)
|
||||
if rc == 0:
|
||||
return stdout
|
||||
else:
|
||||
self._module.fail_json(msg="Error message: " + str(stderr))
|
||||
|
||||
def set_property(self, opendj_bindir, hostname, port, username, password_method, backend_name, name, value):
|
||||
my_command = [
|
||||
opendj_bindir + '/dsconfig',
|
||||
'set-backend-prop',
|
||||
'-h', hostname,
|
||||
'--port', str(port),
|
||||
'--bindDN', username,
|
||||
'--backend-name', backend_name,
|
||||
'--set', name + ":" + value,
|
||||
'-n', '-X'
|
||||
] + password_method
|
||||
rc, stdout, stderr = self._module.run_command(my_command)
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
self._module.fail_json(msg="Error message: " + stderr)
|
||||
|
||||
def validate_data(self, data=None, name=None, value=None):
|
||||
for config_line in data.split('\n'):
|
||||
if config_line:
|
||||
split_line = config_line.split()
|
||||
if split_line[0] == name:
|
||||
if split_line[1] == value:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
opendj_bindir=dict(default="/opt/opendj/bin", type="path"),
|
||||
hostname=dict(required=True),
|
||||
port=dict(required=True),
|
||||
username=dict(default="cn=Directory Manager", required=False),
|
||||
password=dict(required=False, no_log=True),
|
||||
passwordfile=dict(required=False, type="path"),
|
||||
backend=dict(required=True),
|
||||
name=dict(required=True),
|
||||
value=dict(required=True),
|
||||
state=dict(default="present"),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[['password', 'passwordfile']],
|
||||
required_one_of=[['password', 'passwordfile']]
|
||||
)
|
||||
|
||||
opendj_bindir = module.params['opendj_bindir']
|
||||
hostname = module.params['hostname']
|
||||
port = module.params['port']
|
||||
username = module.params['username']
|
||||
password = module.params['password']
|
||||
passwordfile = module.params['passwordfile']
|
||||
backend_name = module.params['backend']
|
||||
name = module.params['name']
|
||||
value = module.params['value']
|
||||
state = module.params['state']
|
||||
|
||||
if module.params["password"] is not None:
|
||||
password_method = ['-w', password]
|
||||
elif module.params["passwordfile"] is not None:
|
||||
password_method = ['-j', passwordfile]
|
||||
|
||||
opendj = BackendProp(module)
|
||||
validate = opendj.get_property(opendj_bindir=opendj_bindir,
|
||||
hostname=hostname,
|
||||
port=port,
|
||||
username=username,
|
||||
password_method=password_method,
|
||||
backend_name=backend_name)
|
||||
|
||||
if validate:
|
||||
if not opendj.validate_data(data=validate, name=name, value=value):
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
if opendj.set_property(opendj_bindir=opendj_bindir,
|
||||
hostname=hostname,
|
||||
port=port,
|
||||
username=username,
|
||||
password_method=password_method,
|
||||
backend_name=backend_name,
|
||||
name=name,
|
||||
value=value):
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue