1"""
2Manage RabbitMQ Users
3=====================
4
5Example:
6
7.. code-block:: yaml
8
9    rabbit_user:
10      rabbitmq_user.present:
11        - password: password
12        - force: True
13        - tags:
14          - monitoring
15          - user
16        - perms:
17          - '/':
18            - '.*'
19            - '.*'
20            - '.*'
21        - runas: rabbitmq
22"""
23
24
25import logging
26
27import salt.utils.path
28from salt.exceptions import CommandExecutionError
29
30log = logging.getLogger(__name__)
31
32
33def __virtual__():
34    """
35    Only load if RabbitMQ is installed.
36    """
37    if salt.utils.path.which("rabbitmqctl"):
38        return True
39    return (False, "Command not found: rabbitmqctl")
40
41
42def _check_perms_changes(name, newperms, runas=None, existing=None):
43    """
44    Check whether Rabbitmq user's permissions need to be changed.
45    """
46    if not newperms:
47        return False
48
49    if existing is None:
50        try:
51            existing = __salt__["rabbitmq.list_user_permissions"](name, runas=runas)
52        except CommandExecutionError as err:
53            log.error("Error: %s", err)
54            return False
55
56    empty_perms = {"configure": "", "write": "", "read": ""}
57    perm_need_change = False
58    for vhost_perms in newperms:
59        for vhost, perms in vhost_perms.items():
60            if vhost in existing:
61                new_perms = {"configure": perms[0], "write": perms[1], "read": perms[2]}
62                existing_vhost = existing[vhost]
63                if new_perms != existing_vhost:
64                    # This checks for setting permissions to nothing in the state,
65                    # when previous state runs have already set permissions to
66                    # nothing. We don't want to report a change in this case.
67                    if existing_vhost == empty_perms and perms == empty_perms:
68                        continue
69                    perm_need_change = True
70            else:
71                perm_need_change = True
72
73    return perm_need_change
74
75
76def _get_current_tags(name, runas=None):
77    """
78    Whether Rabbitmq user's tags need to be changed
79    """
80    try:
81        return list(__salt__["rabbitmq.list_users"](runas=runas)[name])
82    except CommandExecutionError as err:
83        log.error("Error: %s", err)
84        return []
85
86
87def present(name, password=None, force=False, tags=None, perms=(), runas=None):
88    """
89    Ensure the RabbitMQ user exists.
90
91    name
92        User name
93    password
94        The user's password
95    force
96        If force is ``True``, the password will be automatically updated without extra password change check.
97    tags
98        Optional list of tags for the user
99    perms
100        A list of dicts with vhost keys and 3-tuple values
101    runas
102        Name of the user to run the command
103    """
104    ret = {"name": name, "result": False, "comment": "", "changes": {}}
105
106    try:
107        user = __salt__["rabbitmq.user_exists"](name, runas=runas)
108    except CommandExecutionError as err:
109        ret["comment"] = "Error: {}".format(err)
110        return ret
111
112    passwd_reqs_update = False
113    if user and password is not None:
114        try:
115            if not __salt__["rabbitmq.check_password"](name, password, runas=runas):
116                passwd_reqs_update = True
117                log.debug("RabbitMQ user %s password update required", name)
118        except CommandExecutionError as err:
119            ret["comment"] = "Error: {}".format(err)
120            return ret
121
122    if user and not any((force, perms, tags, passwd_reqs_update)):
123        log.debug(
124            "RabbitMQ user '%s' exists, password is up to date and force is not set.",
125            name,
126        )
127        ret["comment"] = "User '{}' is already present.".format(name)
128        ret["result"] = True
129        return ret
130
131    if not user:
132        ret["changes"].update({"user": {"old": "", "new": name}})
133        if __opts__["test"]:
134            ret["result"] = None
135            ret["comment"] = "User '{}' is set to be created.".format(name)
136            return ret
137
138        log.debug("RabbitMQ user '%s' doesn't exist - Creating.", name)
139        try:
140            __salt__["rabbitmq.add_user"](name, password, runas=runas)
141        except CommandExecutionError as err:
142            ret["comment"] = "Error: {}".format(err)
143            return ret
144    else:
145        log.debug("RabbitMQ user '%s' exists", name)
146        if force or passwd_reqs_update:
147            if password is not None:
148                if not __opts__["test"]:
149                    try:
150                        __salt__["rabbitmq.change_password"](
151                            name, password, runas=runas
152                        )
153                    except CommandExecutionError as err:
154                        ret["comment"] = "Error: {}".format(err)
155                        return ret
156                ret["changes"].update({"password": {"old": "", "new": "Set password."}})
157            else:
158                if not __opts__["test"]:
159                    log.debug("Password for %s is not set - Clearing password.", name)
160                    try:
161                        __salt__["rabbitmq.clear_password"](name, runas=runas)
162                    except CommandExecutionError as err:
163                        ret["comment"] = "Error: {}".format(err)
164                        return ret
165                ret["changes"].update(
166                    {"password": {"old": "Removed password.", "new": ""}}
167                )
168
169    if tags is not None:
170        current_tags = _get_current_tags(name, runas=runas)
171        if isinstance(tags, str):
172            tags = tags.split()
173        # Diff the tags sets. Symmetric difference operator ^ will give us
174        # any element in one set, but not both
175        if set(tags) ^ set(current_tags):
176            if not __opts__["test"]:
177                try:
178                    __salt__["rabbitmq.set_user_tags"](name, tags, runas=runas)
179                except CommandExecutionError as err:
180                    ret["comment"] = "Error: {}".format(err)
181                    return ret
182            ret["changes"].update({"tags": {"old": current_tags, "new": tags}})
183    try:
184        existing_perms = __salt__["rabbitmq.list_user_permissions"](name, runas=runas)
185    except CommandExecutionError as err:
186        ret["comment"] = "Error: {}".format(err)
187        return ret
188
189    if _check_perms_changes(name, perms, runas=runas, existing=existing_perms):
190        for vhost_perm in perms:
191            for vhost, perm in vhost_perm.items():
192                if not __opts__["test"]:
193                    try:
194                        __salt__["rabbitmq.set_permissions"](
195                            vhost, name, perm[0], perm[1], perm[2], runas=runas
196                        )
197                    except CommandExecutionError as err:
198                        ret["comment"] = "Error: {}".format(err)
199                        return ret
200                new_perms = {
201                    vhost: {"configure": perm[0], "write": perm[1], "read": perm[2]}
202                }
203                if vhost in existing_perms:
204                    if existing_perms[vhost] != new_perms[vhost]:
205                        if ret["changes"].get("perms") is None:
206                            ret["changes"].update({"perms": {"old": {}, "new": {}}})
207                        ret["changes"]["perms"]["old"].update(existing_perms[vhost])
208                        ret["changes"]["perms"]["new"].update(new_perms)
209                else:
210                    ret["changes"].update({"perms": {"new": {}}})
211                    ret["changes"]["perms"]["new"].update(new_perms)
212
213    ret["result"] = True
214    if ret["changes"] == {}:
215        ret["comment"] = "'{}' is already in the desired state.".format(name)
216        return ret
217
218    if __opts__["test"]:
219        ret["result"] = None
220        ret["comment"] = "Configuration for '{}' will change.".format(name)
221        return ret
222
223    ret["comment"] = "'{}' was configured.".format(name)
224    return ret
225
226
227def absent(name, runas=None):
228    """
229    Ensure the named user is absent
230
231    name
232        The name of the user to remove
233    runas
234        User to run the command
235    """
236    ret = {"name": name, "result": False, "comment": "", "changes": {}}
237
238    try:
239        user_exists = __salt__["rabbitmq.user_exists"](name, runas=runas)
240    except CommandExecutionError as err:
241        ret["comment"] = "Error: {}".format(err)
242        return ret
243
244    if user_exists:
245        if not __opts__["test"]:
246            try:
247                __salt__["rabbitmq.delete_user"](name, runas=runas)
248            except CommandExecutionError as err:
249                ret["comment"] = "Error: {}".format(err)
250                return ret
251        ret["changes"].update({"name": {"old": name, "new": ""}})
252    else:
253        ret["result"] = True
254        ret["comment"] = "The user '{}' is not present.".format(name)
255        return ret
256
257    if __opts__["test"] and ret["changes"]:
258        ret["result"] = None
259        ret["comment"] = "The user '{}' will be removed.".format(name)
260        return ret
261
262    ret["result"] = True
263    ret["comment"] = "The user '{}' was removed.".format(name)
264    return ret
265