diff --git a/README-servicedelegationrule.md b/README-servicedelegationrule.md
new file mode 100644
index 0000000000000000000000000000000000000000..3743eeb5ddd6bd9268bfec96db3d3ca3c047e341
--- /dev/null
+++ b/README-servicedelegationrule.md
@@ -0,0 +1,172 @@
+Servicedelegationrule module
+============
+
+Description
+-----------
+
+The servicedelegationrule module allows to ensure presence and absence of servicedelegationrules and servicedelegationrule members.
+
+Features
+--------
+
+* Servicedelegationrule management
+
+
+Supported FreeIPA Versions
+--------------------------
+
+FreeIPA versions 4.4.0 and up are supported by the ipaservicedelegationrule module.
+
+Host princpals are only usable with IPA versions 4.9.0 and up.
+
+
+Requirements
+------------
+
+**Controller**
+* Ansible version: 2.8+
+
+**Node**
+* Supported FreeIPA version (see above)
+
+
+Usage
+=====
+
+Example inventory file
+
+```ini
+[ipaserver]
+ipaserver.test.local
+```
+
+
+Example playbook to make sure servicedelegationrule delegation-rule is present:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationrule
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationrule delegation-rule is present
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-rule
+```
+
+
+Example playbook to make sure servicedelegationrule delegation-rule member principal test/example.com is present:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationrule
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationrule delegation-rule member principal test/example.com is present
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-rule
+      principal: test/example.com
+      action: member
+```
+
+
+Example playbook to make sure servicedelegationrule delegation-rule member principal test/example.com is absent:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationrule
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationrule delegation-rule member principal test/example.com is absent
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-rule
+      principal: test/example.com
+      action: member
+      state: absent
+    state: absent
+```
+
+
+Example playbook to make sure servicedelegationrule delegation-rule member target delegation-target is present:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationrule
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationrule delegation-rule member target delegation-target is present
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-rule
+      target: delegation-target
+      action: member
+```
+
+
+Example playbook to make sure servicedelegationrule delegation-rule member target delegation-target is absent:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationrule
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationrule delegation-rule member target delegation-target is absent
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-rule
+      target: delegation-target
+      action: member
+      state: absent
+    state: absent
+```
+
+
+Example playbook to make sure servicedelegationrule delegation-rule is absent:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationrule
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationrule delegation-rule is absent
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-rule
+      state: absent
+```
+
+
+Variables
+---------
+
+Variable | Description | Required
+-------- | ----------- | --------
+`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
+`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
+`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no
+`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to yes. (bool) | no
+`name` \| `cn` | The list of servicedelegationrule name strings. | yes
+`principal` |  The list of principals. A principal can be of the format: fqdn, fqdn@REALM, service/fqdn, service/fqdn@REALM, host/fqdn, host/fqdn@REALM, alias$, alias$@REALM, where fqdn and fqdn@REALM are host principals and the same as host/fqdn and host/fqdn@REALM. Host princpals are only usable with IPA versions 4.9.0 and up. | no
+`target` \| `servicedelegationtarget` | The list of service delegation targets. | no
+`action` | Work on servicedelegationrule or member level. It can be on of `member` or `servicedelegationrule` and defaults to `servicedelegationrule`. | no
+`state` | The state to ensure. It can be one of `present`, `absent`, default: `present`. | no
+
+
+Authors
+=======
+
+Thomas Woerner
diff --git a/README.md b/README.md
index a50b7d82bffa9322044079ce37ed0e3112a2b515..ea53a5230d980dc814de08f0b8e793a07998dbe2 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@ Features
 * Modules for self service management
 * Modules for server management
 * Modules for service management
+* Modules for service delegation rule management
 * Modules for service delegation target management
 * Modules for sudocmd management
 * Modules for sudocmdgroup management
@@ -451,6 +452,7 @@ Modules in plugin/modules
 * [ipaselfservice](README-selfservice.md)
 * [ipaserver](README-server.md)
 * [ipaservice](README-service.md)
+* [ipaservicedelegationrule](README-servicedelegationrule.md)
 * [ipaservicedelegationtarget](README-servicedelegationtarget.md)
 * [ipasudocmd](README-sudocmd.md)
 * [ipasudocmdgroup](README-sudocmdgroup.md)
diff --git a/playbooks/servicedelegationrule/servicedelegationrule-absent.yml b/playbooks/servicedelegationrule/servicedelegationrule-absent.yml
new file mode 100644
index 0000000000000000000000000000000000000000..599561576e7458dbb15d99179f7b652a6874184d
--- /dev/null
+++ b/playbooks/servicedelegationrule/servicedelegationrule-absent.yml
@@ -0,0 +1,10 @@
+---
+- name: Servicedelegationrule absent example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationrule test-delegation-rule is absent
+    ipaservicedelegationrule:
+     name: test-delegation-rule
+     state: absent
diff --git a/playbooks/servicedelegationrule/servicedelegationrule-present.yml b/playbooks/servicedelegationrule/servicedelegationrule-present.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8e3ccd49ac60ac825f6985d21ec237e87398082b
--- /dev/null
+++ b/playbooks/servicedelegationrule/servicedelegationrule-present.yml
@@ -0,0 +1,9 @@
+---
+- name: Servicedelegationrule present example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationrule test-delegation-rule is present
+    ipaservicedelegationrule:
+     name: test-delegation-rule
diff --git a/playbooks/servicedelegationrule/servicedelegationrule-principal-member-absent.yml b/playbooks/servicedelegationrule/servicedelegationrule-principal-member-absent.yml
new file mode 100644
index 0000000000000000000000000000000000000000..784da69a8b697a3d3ae6e722f75586de1a09b058
--- /dev/null
+++ b/playbooks/servicedelegationrule/servicedelegationrule-principal-member-absent.yml
@@ -0,0 +1,12 @@
+---
+- name: Servicedelegationrule principal member absent example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure principal member test/example.com is absent in servicedelegationrule test-delegation-rule
+    ipaservicedelegationrule:
+     name: test-delegation-rule
+     principal: test/example.com
+     action: member
+     state: absent
diff --git a/playbooks/servicedelegationrule/servicedelegationrule-principal-member-present.yml b/playbooks/servicedelegationrule/servicedelegationrule-principal-member-present.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d8f960583e803da6fff4860eba4a571d5626f8c4
--- /dev/null
+++ b/playbooks/servicedelegationrule/servicedelegationrule-principal-member-present.yml
@@ -0,0 +1,11 @@
+---
+- name: Servicedelegationrule principal member present example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure principal member test/example.com is present in servicedelegationrule test-delegation-rule
+    ipaservicedelegationrule:
+     name: test-delegation-rule
+     principal: test/example.com
+     action: member
diff --git a/playbooks/servicedelegationrule/servicedelegationrule-target-member-absent.yml b/playbooks/servicedelegationrule/servicedelegationrule-target-member-absent.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cb280e4f38c409f24241f94a6f5c47b21d7aa5f3
--- /dev/null
+++ b/playbooks/servicedelegationrule/servicedelegationrule-target-member-absent.yml
@@ -0,0 +1,12 @@
+---
+- name: Servicedelegationrule absent example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure member test/example.com is absent in servicedelegationrule test-delegation-rule
+    ipaservicedelegationrule:
+     name: test-delegation-rule
+     principal: test/example.com
+     action: member
+     state: absent
diff --git a/playbooks/servicedelegationrule/servicedelegationrule-target-member-present.yml b/playbooks/servicedelegationrule/servicedelegationrule-target-member-present.yml
new file mode 100644
index 0000000000000000000000000000000000000000..257971d147b88f63d4f3a3ab1f81177dea1047a0
--- /dev/null
+++ b/playbooks/servicedelegationrule/servicedelegationrule-target-member-present.yml
@@ -0,0 +1,11 @@
+---
+- name: Servicedelegationrule member present example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure member test/example.com is present in servicedelegationrule test-delegation-rule
+    ipaservicedelegationrule:
+     name: test-delegation-rule
+     principal: test/example.com
+     action: member
diff --git a/plugins/modules/ipaservicedelegationrule.py b/plugins/modules/ipaservicedelegationrule.py
new file mode 100644
index 0000000000000000000000000000000000000000..93e41b46f554c15396137976ff52938202601f63
--- /dev/null
+++ b/plugins/modules/ipaservicedelegationrule.py
@@ -0,0 +1,352 @@
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 2022 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+    "metadata_version": "1.0",
+    "supported_by": "community",
+    "status": ["preview"],
+}
+
+DOCUMENTATION = """
+---
+module: ipaservicedelegationrule
+short description: Manage FreeIPA servicedelegationrule
+description: |
+  Manage FreeIPA servicedelegationrule and servicedelegationrule members
+extends_documentation_fragment:
+  - ipamodule_base_docs
+options:
+  name:
+    description: The list of servicedelegationrule name strings.
+    required: true
+    aliases: ["cn"]
+  principal:
+    description: |
+      The list of principals. A principal can be of the format:
+      fqdn, fqdn@REALM, service/fqdn, service/fqdn@REALM, host/fqdn,
+      host/fqdn@REALM, alias$, alias$@REALM, where fqdn and fqdn@REALM
+      are host principals and the same as host/fqdn and host/fqd
+      Host princpals are only usable with IPA versions 4.9.0 and up.
+    required: false
+  target:
+    description: |
+      The list of service delegation targets.
+    required: false
+    aliases: ["servicedelegationtarget"]
+  action:
+    description: Work on servicedelegationrule or member level.
+    choices: ["servicedelegationrule", "member"]
+    default: servicedelegationrule
+    required: false
+  state:
+    description: The state to ensure.
+    choices: ["present", "absent"]
+    default: present
+    required: true
+"""
+
+EXAMPLES = """
+# Ensure servicedelegationrule delegation-rule is present
+- ipaservicedelegationrule:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-rule
+
+# Ensure servicedelegationrule delegation-rule member principal
+# test/example.com is present
+- ipaservicedelegationrule:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-rule
+    principal: test/example.com
+    action: member
+
+# Ensure servicedelegationrule delegation-rule member principal
+# test/example.com is absent
+- ipaservicedelegationrule:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-rule
+    principal: test/example.com
+    action: member
+    state: absent
+
+# Ensure servicedelegationrule delegation-rule member target
+# test/example.com is present
+- ipaservicedelegationrule:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-rule
+    target: delegation-target
+    action: member
+
+# Ensure servicedelegationrule delegation-rule member target
+# test/example.com is absent
+- ipaservicedelegationrule:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-rule
+    target: delegation-target
+    action: member
+    state: absent
+
+# Ensure servicedelegationrule delegation-rule is absent
+- ipaservicedelegationrule:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-rule
+    state: absent
+"""
+
+RETURN = """
+"""
+
+
+from ansible.module_utils.ansible_freeipa_module import \
+    IPAAnsibleModule, gen_add_del_lists, gen_add_list, gen_intersection_list, \
+    servicedelegation_normalize_principals, ipalib_errors
+from ansible.module_utils import six
+
+if six.PY3:
+    unicode = str
+
+
+def find_servicedelegationrule(module, name):
+    """Find if a servicedelegationrule with the given name already exist."""
+    try:
+        _result = module.ipa_command("servicedelegationrule_show", name,
+                                     {"all": True})
+    except Exception:  # pylint: disable=broad-except
+        # An exception is raised if servicedelegationrule name is not found.
+        return None
+    else:
+        return _result["result"]
+
+
+def check_targets(module, targets):
+    def _check_exists(module, _type, name):
+        # Check if item of type _type exists using the show command
+        try:
+            module.ipa_command("%s_show" % _type, name, {})
+        except ipalib_errors.NotFound as e:
+            msg = str(e)
+            if "%s not found" % _type in msg:
+                return False
+            module.fail_json(msg="%s_show failed: %s" % (_type, msg))
+        return True
+
+    for _target in targets:
+        if not _check_exists(module, "servicedelegationtarget", _target):
+            module.fail_json(
+                msg="Service delegation target '%s' does not exist" % _target)
+
+
+def main():
+    ansible_module = IPAAnsibleModule(
+        argument_spec=dict(
+            # general
+            name=dict(type="list", aliases=["cn"], default=None,
+                      required=True),
+            # present
+            principal=dict(required=False, type='list', default=None),
+            target=dict(required=False, type='list',
+                        aliases=["servicedelegationtarget"], default=None),
+
+            action=dict(type="str", default="servicedelegationrule",
+                        choices=["member", "servicedelegationrule"]),
+            # state
+            state=dict(type="str", default="present",
+                       choices=["present", "absent"]),
+        ),
+        supports_check_mode=True,
+    )
+
+    ansible_module._ansible_debug = True
+
+    # Get parameters
+
+    # general
+    names = ansible_module.params_get("name")
+
+    # present
+    principal = ansible_module.params_get("principal")
+    target = ansible_module.params_get("target")
+
+    action = ansible_module.params_get("action")
+
+    # state
+    state = ansible_module.params_get("state")
+
+    # Check parameters
+
+    invalid = []
+
+    if state == "present":
+        if len(names) != 1:
+            ansible_module.fail_json(
+                msg="Only one servicedelegationrule can be added at a time.")
+
+    if state == "absent":
+        if len(names) < 1:
+            ansible_module.fail_json(msg="No name given.")
+        if action == "servicedelegationrule":
+            invalid = ["principal", "target"]
+
+    ansible_module.params_fail_used_invalid(invalid, state, action)
+
+    # Init
+
+    membertarget = "ipaallowedtarget_servicedelegationtarget"
+    changed = False
+    exit_args = {}
+
+    # Connect to IPA API
+    with ansible_module.ipa_connect():
+
+        # Normalize principals
+        if principal:
+            principal = servicedelegation_normalize_principals(ansible_module,
+                                                               principal)
+        if target:
+            check_targets(ansible_module, target)
+
+        commands = []
+        principal_add = principal_del = []
+        target_add = target_del = []
+        for name in names:
+            # Make sure servicedelegationrule exists
+            res_find = find_servicedelegationrule(ansible_module, name)
+
+            # Create command
+            if state == "present":
+
+                if action == "servicedelegationrule":
+                    # A servicedelegationrule does not have normal options.
+                    # There is no servicedelegationtarget-mod command.
+                    # Principal members are handled with the _add_member and
+                    # _remove_member commands further down.
+                    if res_find is None:
+                        commands.append([name, "servicedelegationrule_add",
+                                         {}])
+                        res_find = {}
+
+                    # Generate addition and removal lists for principal
+                    principal_add, principal_del = gen_add_del_lists(
+                        principal, res_find.get("memberprincipal"))
+
+                    # Generate addition and removal lists for target
+                    target_add, target_del = gen_add_del_lists(
+                        target, res_find.get(membertarget))
+
+                elif action == "member":
+                    if res_find is None:
+                        ansible_module.fail_json(
+                            msg="No servicedelegationrule '%s'" % name)
+
+                    # Reduce add lists for principal
+                    # to new entries only that are not in res_find.
+                    if principal is not None and \
+                       "memberprincipal" in res_find:
+                        principal_add = gen_add_list(
+                            principal, res_find["memberprincipal"])
+                    else:
+                        principal_add = principal
+
+                    # Reduce add lists for target
+                    # to new entries only that are not in res_find.
+                    if target is not None and membertarget in res_find:
+                        target_add = gen_add_list(
+                            target, res_find[membertarget])
+                    else:
+                        target_add = target
+
+            elif state == "absent":
+                if action == "servicedelegationrule":
+                    if res_find is not None:
+                        commands.append([name, "servicedelegationrule_del",
+                                         {}])
+
+                elif action == "member":
+                    if res_find is None:
+                        ansible_module.fail_json(
+                            msg="No servicedelegationrule '%s'" % name)
+
+                    # Reduce del lists of principals to the entries only
+                    # that are in res_find.
+                    if principal is not None:
+                        principal_del = gen_intersection_list(
+                            principal, res_find.get("memberprincipal"))
+                    else:
+                        principal_del = principal
+
+                    # Reduce del lists of targets to the entries only
+                    # that are in res_find.
+                    if target is not None:
+                        target_del = gen_intersection_list(
+                            target, res_find.get(membertarget))
+                    else:
+                        target_del = target
+
+            else:
+                ansible_module.fail_json(msg="Unkown state '%s'" % state)
+
+            # Handle members
+
+            # Add principal members
+            if principal_add is not None and len(principal_add) > 0:
+                commands.append(
+                    [name, "servicedelegationtarget_add_member",
+                     {
+                         "principal": principal_add,
+                     }])
+            # Remove principal members
+            if principal_del is not None and len(principal_del) > 0:
+                commands.append(
+                    [name, "servicedelegationtarget_remove_member",
+                     {
+                         "principal": principal_del,
+                     }])
+
+            # Add target members
+            if target_add is not None and len(target_add) > 0:
+                commands.append(
+                    [name, "servicedelegationrule_add_target",
+                     {
+                         "servicedelegationtarget": target_add,
+                     }])
+            # Remove target members
+            if target_del is not None and len(target_del) > 0:
+                commands.append(
+                    [name, "servicedelegationrule_remove_target",
+                     {
+                         "servicedelegationtarget": target_del,
+                     }])
+
+        # Execute commands
+
+        changed = ansible_module.execute_ipa_commands(
+            commands, fail_on_member_errors=True)
+
+    # Done
+
+    ansible_module.exit_json(changed=changed, **exit_args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/servicedelegationrule/test_servicedelegationrule.yml b/tests/servicedelegationrule/test_servicedelegationrule.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0e351e8b77de6506fefd563889e6d6938945bd9d
--- /dev/null
+++ b/tests/servicedelegationrule/test_servicedelegationrule.yml
@@ -0,0 +1,188 @@
+---
+- name: Test servicedelegationrule
+  hosts: "{{ ipa_test_host | default('ipaserver') }}"
+  # Change "become" or "gather_facts" to "yes",
+  # if you test playbook requires any.
+  become: no
+  gather_facts: yes
+
+  tasks:
+
+  # CLEANUP TEST ITEMS
+
+  - name: Ensure servicedelegationrule test-delegation-rule is absent
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      state: absent
+
+  - name: Ensure service is absent
+    ipaservice:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      state: absent
+      continue: yes
+
+  - name: Ensure servicedelegationrule test-delegation-target is absent
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+      state: absent
+
+  # CREATE TEST ITEMS
+
+  - name: Ensure service test-sevice is present
+    ipaservice:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-target is present
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+    register: result
+    failed_when: not result.changed or result.failed
+
+  # TESTS
+
+  - name: Ensure servicedelegationrule test-delegation-rule is present
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule is present again
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule member target test-delegation-target is present
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      target: test-delegation-target
+      action: member
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule member target test-delegation-target is present, again
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      target: test-delegation-target
+      action: member
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule member principal "{{ 'test-service/' + ansible_facts['fqdn'] }}" is present
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      principal: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      action: member
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule member principal "{{ 'test-service/' + ansible_facts['fqdn'] }}" is present again
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      principal: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      action: member
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule member principal "{{ 'test-service/' + ansible_facts['fqdn'] }}" is absent
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      principal: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      action: member
+      state: absent
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule member principal "{{ 'test-service/' + ansible_facts['fqdn'] }}" is present absent
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      principal: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      action: member
+      state: absent
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule member target test-delegation-target is absent
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      target: test-delegation-target
+      action: member
+      state: absent
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule member target test-delegation-target is absent, again
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      target: test-delegation-target
+      action: member
+      state: absent
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule is absent
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      state: absent
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationrule test-delegation-rule is absent again
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-rule
+      state: absent
+    register: result
+    failed_when: result.changed or result.failed
+
+  # CLEANUP TEST ITEMS
+
+  - name: Ensure servicedelegationrule test-delegation-target is absent
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+      state: absent
+
+  - name: Ensure service is absent
+    ipaservice:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      state: absent
+      continue: yes
diff --git a/tests/servicedelegationrule/test_servicedelegationrule_client_context.yml b/tests/servicedelegationrule/test_servicedelegationrule_client_context.yml
new file mode 100644
index 0000000000000000000000000000000000000000..03546ffc179fd831eeaaaca9be320810c84ac268
--- /dev/null
+++ b/tests/servicedelegationrule/test_servicedelegationrule_client_context.yml
@@ -0,0 +1,39 @@
+---
+- name: Test servicedelegationrule
+  hosts: ipaclients, ipaserver
+  # Change "become" or "gather_facts" to "yes",
+  # if you test playbook requires any.
+  become: no
+  gather_facts: no
+
+  tasks:
+  - name: Include FreeIPA facts.
+    include_tasks: ../env_freeipa_facts.yml
+
+  # Test will only be executed if host is not a server.
+  - name: Execute with server context in the client.
+    ipaservicedelegationrule:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: server
+      name: ThisShouldNotWork
+    register: result
+    failed_when: not (result.failed and result.msg is regex("No module named '*ipaserver'*"))
+    when: ipa_host_is_client
+
+# Import basic module tests, and execute with ipa_context set to 'client'.
+# If ipaclients is set, it will be executed using the client, if not,
+# ipaserver will be used.
+#
+# With this setup, tests can be executed against an IPA client, against
+# an IPA server using "client" context, and ensure that tests are executed
+# in upstream CI.
+
+- name: Test servicedelegationrule using client context, in client host.
+  import_playbook: test_servicedelegationrule.yml
+  when: groups['ipaclients']
+  vars:
+    ipa_test_host: ipaclients
+
+- name: Test servicedelegationrule using client context, in server host.
+  import_playbook: test_servicedelegationrule.yml
+  when: groups['ipaclients'] is not defined or not groups['ipaclients']
diff --git a/tests/servicedelegationrule/test_servicedelegationrule_hostprincipal.yml b/tests/servicedelegationrule/test_servicedelegationrule_hostprincipal.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f0f79a76e69278b885fb9657895aa06473571ece
--- /dev/null
+++ b/tests/servicedelegationrule/test_servicedelegationrule_hostprincipal.yml
@@ -0,0 +1,148 @@
+---
+- name: Test servicedelegationrule_hostprincipal
+  hosts: "{{ ipa_test_host | default('ipaserver') }}"
+  become: no
+  gather_facts: yes
+
+  tasks:
+  # setup
+  - include_tasks: ../env_freeipa_facts.yml
+
+  # host principals are only possible with IPA 4.9.0+
+  - block:
+
+    # SET FACTS
+
+    - name: Get Domain from server name
+      set_fact:
+        ipaserver_domain: "{{ ansible_facts['fqdn'].split('.')[1:] | join ('.') }}"
+      when: ipaserver_domain is not defined
+
+    - name: Get REALM from server name
+      set_fact:
+        ipaserver_realm: "{{ ipaserver_domain | upper }}"
+      when: ipaserver_realm is not defined
+
+    - name: Set test-host fqdn
+      set_fact:
+        test_host_fqdn: "{{ 'test-host.' + ipaserver_domain }}"
+        test_host_fqdn_realm: "{{ 'test-host.' + ipaserver_domain + '@' + ipaserver_realm }}"
+
+    # CLEANUP TEST ITEMS
+
+    - name: Ensure servicedelegationrule test-delegation-rule is absent
+      ipaservicedelegationrule:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-rule
+        state: absent
+
+    - name: Ensure host is absent
+      ipahost:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: "{{ test_host_fqdn }}"
+        state: absent
+
+    # CREATE TEST ITEMS
+
+    - name: Ensure host is present
+      ipahost:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: "{{ test_host_fqdn }}"
+        force: yes
+
+    - name: Ensure servicedelegationrule test-delegation-rule is present
+      ipaservicedelegationrule:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-rule
+      register: result
+      failed_when: not result.changed or result.failed
+
+    # TESTS
+
+    - name: Ensure servicedelegationrule test-delegation-rule member host principal "{{ test_host_fqdn }}" is present
+      ipaservicedelegationrule:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-rule
+        principal: "{{ test_host_fqdn }}"
+        action: member
+      register: result
+      failed_when: not result.changed or result.failed
+
+    - name: Ensure servicedelegationrule test-delegation-rule member host principal "{{ test_host_fqdn }}" is present again
+      ipaservicedelegationrule:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-rule
+        principal: "{{ test_host_fqdn }}"
+        action: member
+      register: result
+      failed_when: result.changed or result.failed
+
+    - name: Ensure servicedelegationrule test-delegation-rule member host principal "{{ test_host_fqdn_realm }}" is present unchanged
+      ipaservicedelegationrule:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-rule
+        principal: "{{ test_host_fqdn_realm }}"
+        action: member
+      register: result
+      failed_when: result.changed or result.failed
+
+    - name: Ensure servicedelegationrule test-delegation-rule member host principal "{{ 'host/' + test_host_fqdn_realm }}" is present unchanged
+      ipaservicedelegationrule:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-rule
+        principal: "{{ 'host/' + test_host_fqdn_realm }}"
+        action: member
+      register: result
+      failed_when: result.changed or result.failed
+
+    - name: Ensure servicedelegationrule test-delegation-rule member host principal "{{ test_host_fqdn_realm }}" is absent
+      ipaservicedelegationrule:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-rule
+        principal: "{{ test_host_fqdn_realm }}"
+        action: member
+        state: absent
+      register: result
+      failed_when: not result.changed or result.failed
+
+    - name: Ensure servicedelegationrule test-delegation-rule member host principal "{{ test_host_fqdn }}" is absent unchanged
+      ipaservicedelegationrule:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-rule
+        principal: "{{ test_host_fqdn }}"
+        action: member
+        state: absent
+      register: result
+      failed_when: result.changed or result.failed
+
+    # CLEANUP TEST ITEMS
+
+    - name: Ensure servicedelegationrule test-delegation-rule is absent
+      ipaservicedelegationrule:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-rule
+        state: absent
+      register: result
+      failed_when: not result.changed or result.failed
+
+    - name: Ensure host is absent
+      ipahost:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: "{{ test_host_fqdn }}"
+        state: absent
+      register: result
+      failed_when: not result.changed or result.failed
+
+    when: ipa_version is version('4.9.0', '>=')