From 98769717c451f8c253de695dd23f0614d7341b29 Mon Sep 17 00:00:00 2001
From: Adrien Waksberg <git@waks.be>
Date: Thu, 16 Nov 2023 16:05:55 +0100
Subject: [PATCH] feat: manage role

---
 CHANGELOG.md                           |   1 +
 README.md                              |  19 +++-
 defaults/main.yml                      |   2 +
 library/elasticsearch_role.py          | 126 +++++++++++++++++++++++++
 molecule/default/converge.yml          |   9 ++
 molecule/default/tests/test_default.py |   5 +
 tasks/data.yml                         |  14 +++
 7 files changed, 173 insertions(+), 3 deletions(-)
 create mode 100644 library/elasticsearch_role.py

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a2693b..1fe6c93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ Which is based on [Keep A Changelog](http://keepachangelog.com/)
 
 - feat: add variable to set master
 - feat: manage user
+- feat: manage role
 - feat: add variable to set major version
 - feat: add ilm policy
 - test: add support debian 12
diff --git a/README.md b/README.md
index 47b10c5..19767e4 100644
--- a/README.md
+++ b/README.md
@@ -21,13 +21,26 @@ Install and configure Elasticsearch
 * `elasticsearch_api_password` - set the password for api
 * `elasticsearch_config` - hash with the configuration (see [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html))
 
-```
+```yaml
   path.data: /var/lib/elasticsearch
   path.logs: /var/log/elasticsearch
 ```
 
 * `elasticsearch_ssl_key`: - string contain ssl private key if `xpack.security.transport.ssl.key` is defined in elasticsearch_config
 * `elasticsearch_ssl_certificate`: - string contain ssl certificate if `xpack.security.transport.certificate.key` is defined in elasticsearch_config
+* `elasticsearch_roles` - hash with the roles to managed
+
+```yaml
+  myrole:
+    cluster:
+      - all
+    indices:
+      - names: ["logstash*"]
+        privileges:
+          - create
+          - write
+```
+
 * `elasticsearch_users` - hash with the users to managed
 
 ```yaml
@@ -41,7 +54,7 @@ Install and configure Elasticsearch
 
 * `elasticsearch_index_templates` - hash with the index templates configuration
 
-```
+```yaml
   logstash:
     index_patterns:
       - 'logstash-*'
@@ -58,7 +71,7 @@ Install and configure Elasticsearch
 
 * `elasticsearch_ilm_policies` - hash with the ilm policies configuration
 
-```
+```yaml
   autoclean:
     delete:
       min_age: 30d
diff --git a/defaults/main.yml b/defaults/main.yml
index 3482ece..3a77b30 100644
--- a/defaults/main.yml
+++ b/defaults/main.yml
@@ -12,3 +12,5 @@ elasticsearch_default_config:
 elasticsearch_full_config: "{{ elasticsearch_default_config | combine(elasticsearch_config) }}"
 elasticsearch_index_templates: {}
 elasticsearch_ilm_policies: {}
+elasticsearch_roles: {}
+elasticsearch_users: {}
diff --git a/library/elasticsearch_role.py b/library/elasticsearch_role.py
new file mode 100644
index 0000000..31e68a1
--- /dev/null
+++ b/library/elasticsearch_role.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+
+from ansible.module_utils.basic import *
+from ansible.module_utils.elasticsearch_api import *
+
+class ElasticsearchRole:
+    def __init__(self, api_url, api_user, api_password, name, cluster, indices):
+        self.api = ElasticsearchApi(
+            api_url,
+            api_user,
+            api_password
+        )
+        self.api_url  = api_url
+        self.name     = name
+        self.cluster  = cluster
+        self.indices  = indices
+        self.exist    = False
+        self.data     = {}
+
+    def get_data(self):
+        status_code, data = self.api.get('_security/role/{}'.format(self.name))
+        if status_code == 200:
+            self.exist = True
+            self.data  = data[self.name]
+
+    def array_has_changed(self, list1, list2):
+        for item in list1:
+            if item not in list2:
+                return True
+
+        for item in list2:
+            if item not in list1:
+                return True
+
+        return False
+
+    def cluster_has_changed(self):
+        return self.array_has_changed(self.cluster, self.data['cluster'])
+
+    def same_indice(self, indice1, indice2):
+        if self.array_has_changed(indice1['names'], indice2['names']):
+            return False
+
+        if self.array_has_changed(indice1['privileges'], indice2['privileges']):
+            return False
+        
+        return True
+
+    def indices_have_changed(self):
+        for indice1 in self.indices:
+            exist = False
+            for indice2 in self.data['indices']:
+                if self.same_indice(indice1, indice2):
+                    exist = True
+                    break
+            if not exist:
+                return True
+
+        for indice1 in self.data['indices']:
+            exist = False
+            for indice2 in self.indices:
+                if self.same_indice(indice1, indice2):
+                    exist = True
+                    break
+            if not exist:
+                return True
+
+        return False
+
+    def has_changed(self):
+        if self.cluster_has_changed():
+            return True
+
+        if self.indices_have_changed():
+            return True
+
+        return False
+
+    def create(self):
+        self.api.put(
+            '_security/role/{}'.format(self.name),
+            {
+                'cluster': self.cluster,
+                'indices': self.indices
+            }
+        )
+
+    def delete(self):
+        self.api.delete('_security/role/{}'.format(self.name))
+
+        
+def main():
+    fields = {
+        'name':         { 'type': 'str', 'required': True },
+        'indices':      { 'type': 'list', 'default': [] },
+        'cluster':      { 'type': 'list', 'default': [] },
+        'api_url':      { 'type': 'str', 'default': 'http://127.0.0.1:9200' },
+        'api_user':     { 'type': 'str', 'default': None },
+        'api_password': { 'type': 'str', 'default': None, 'no_log': True },
+        'state':        { 'type': 'str', 'default': 'present', 'choice': ['present', 'absent'] },
+    }
+    module = AnsibleModule(argument_spec=fields)
+    changed = False
+
+    role = ElasticsearchRole(
+        module.params['api_url'],
+        module.params['api_user'],
+        module.params['api_password'],
+        module.params['name'],
+        module.params['cluster'],
+        module.params['indices'],
+    )
+    role.get_data()
+
+    if module.params['state'] == 'present':
+        if not role.exist or role.has_changed():
+            role.create()
+            changed = True
+    elif user.exist:
+        role.delete()
+        changed = True
+
+    module.exit_json(changed=changed)
+
+if __name__ == '__main__':
+    main()
diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml
index c99069d..46728b3 100644
--- a/molecule/default/converge.yml
+++ b/molecule/default/converge.yml
@@ -60,6 +60,15 @@
       F3OGK/Tmx3MW5O+lq5O+2oRmUSfcIUgnrjgUeevj6Rgt1qx33WEoKBM2rVBIBqOn
       ZKrzDBkVG/H+H0hwiV219PLE
       -----END PRIVATE KEY-----
+    elasticsearch_roles:
+      myrole:
+        cluster:
+          - all
+        indices:
+          - names: ["logstash*"]
+            privileges:
+              - create
+              - write
     elasticsearch_users:
       toto:
         password: supers3cret
diff --git a/molecule/default/tests/test_default.py b/molecule/default/tests/test_default.py
index 6a7ea77..c01dd13 100644
--- a/molecule/default/tests/test_default.py
+++ b/molecule/default/tests/test_default.py
@@ -45,6 +45,11 @@ def test_elasticsearch_template(host):
     result = host.check_output('curl -v -u elastic:mysecret http://127.0.0.1:9200/_template/test')
     assert '"number_of_replicas":"1"' in result
 
+def test_elasticsearch_role(host):
+    result = host.check_output('curl -v -u elastic:mysecret http://127.0.0.1:9200/_security/role/myrole')
+    assert '"names":["logstash*"]' in result
+    assert '"privileges":["create","write"]' in result
+
 def test_elasticsearch_user(host):
     result = host.check_output('curl -v -u elastic:mysecret http://127.0.0.1:9200/_security/user/toto')
     assert '"username":"toto"' in result
diff --git a/tasks/data.yml b/tasks/data.yml
index ddf8481..fa3c9ff 100644
--- a/tasks/data.yml
+++ b/tasks/data.yml
@@ -6,6 +6,20 @@
   run_once: true
   tags: elasticsearch
 
+- name: Manage roles
+  elasticsearch_role:
+    name: "{{ item.key }}"
+    cluster: "{{ item.value.cluster | default(omit) }}"
+    indices: "{{ item.value.indices | default(omit) }}"
+    api_user: "{{ elasticsearch_api_user }}"
+    api_password: "{{ elasticsearch_password }}"
+    state: "{{ item.value.state | default('present') }}"
+  loop: "{{ elasticsearch_roles | dict2items }}"
+  loop_control:
+    label: "{{ item.key }}"
+  run_once: true
+  tags: elasticsearch
+
 - name: Manage users
   elasticsearch_user:
     name: "{{ item.key }}"