diff --git a/integration/tests/posit/connect/test_content_item_permissions.py b/integration/tests/posit/connect/test_content_item_permissions.py
index 489bf543..518178d5 100644
--- a/integration/tests/posit/connect/test_content_item_permissions.py
+++ b/integration/tests/posit/connect/test_content_item_permissions.py
@@ -1,5 +1,14 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+
 from posit import connect
-from posit.connect.content import ContentItem
+
+if TYPE_CHECKING:
+    from posit.connect.content import ContentItem
+    from posit.connect.permissions import Permission
 
 
 class TestContentPermissions:
@@ -48,7 +57,7 @@ def test_permissions_add_destroy(self):
             role="owner",
         )
 
-        def assert_permissions_match_guids(permissions, objs_with_guid):
+        def assert_permissions_match_guids(permissions: list[Permission], objs_with_guid):
             for permission, obj_with_guid in zip(permissions, objs_with_guid):
                 assert permission["principal_guid"] == obj_with_guid["guid"]
 
@@ -59,11 +68,9 @@ def assert_permissions_match_guids(permissions, objs_with_guid):
         )
 
         # Remove permissions (and from some that isn't an owner)
-        destroyed_permissions = self.content.permissions.destroy(self.user_aron, self.user_bill)
-        assert_permissions_match_guids(
-            destroyed_permissions,
-            [self.user_aron],
-        )
+        self.content.permissions.destroy(self.user_aron)
+        with pytest.raises(ValueError):
+            self.content.permissions.destroy(self.user_bill)
 
         # Prove they have been removed
         assert_permissions_match_guids(
@@ -72,11 +79,7 @@ def assert_permissions_match_guids(permissions, objs_with_guid):
         )
 
         # Remove the last permission
-        destroyed_permissions = self.content.permissions.destroy(self.group_friends)
-        assert_permissions_match_guids(
-            destroyed_permissions,
-            [self.group_friends],
-        )
+        self.content.permissions.destroy(self.group_friends)
 
         # Prove they have been removed
         assert self.content.permissions.find() == []
diff --git a/integration/tests/posit/connect/test_users.py b/integration/tests/posit/connect/test_users.py
index ed4efbad..efb3c72c 100644
--- a/integration/tests/posit/connect/test_users.py
+++ b/integration/tests/posit/connect/test_users.py
@@ -51,6 +51,48 @@ def test_get(self):
         assert self.client.users.get(self.bill["guid"]) == self.bill
         assert self.client.users.get(self.cole["guid"]) == self.cole
 
+    # Also tests Groups.members
+    def test_user_group_interactions(self):
+        try:
+            test_group = self.client.groups.create(name="UnitFriends")
+
+            # `Group.members.count()`
+            assert test_group.members.count() == 0
+
+            # `Group.members.add()`
+            test_group.members.add(self.bill)
+            # `User.groups.add()`
+            assert test_group.members.count() == 1
+            self.cole.groups.add(test_group)
+            assert test_group.members.count() == 2
+
+            # `Group.members.find()`
+            group_users = test_group.members.find()
+            assert len(group_users) == 2
+            assert group_users[0]["guid"] == self.bill["guid"]
+            assert group_users[1]["guid"] == self.cole["guid"]
+
+            # `User.group.find()`
+            bill_groups = self.bill.groups.find()
+            assert len(bill_groups) == 1
+            assert bill_groups[0]["guid"] == test_group["guid"]
+
+            # `Group.members.delete()`
+            test_group.members.delete(self.bill)
+            assert test_group.members.count() == 1
+
+            # `User.groups.delete()`
+            self.cole.groups.delete(test_group)
+            assert test_group.members.count() == 0
+
+        finally:
+            groups = self.client.groups.find(prefix="UnitFriends")
+            if len(groups) > 0:
+                test_group = groups[0]
+                test_group.delete()
+
+            assert len(self.client.groups.find(prefix="UnitFriends")) == 0
+
 
 class TestUserContent:
     """Checks behavior of the content attribute."""
diff --git a/src/posit/connect/client.py b/src/posit/connect/client.py
index e1ba808c..42cff275 100644
--- a/src/posit/connect/client.py
+++ b/src/posit/connect/client.py
@@ -156,7 +156,7 @@ def __init__(self, *args, **kwargs) -> None:
         session.hooks["response"].append(hooks.handle_errors)
         self.session = session
         self.resource_params = ResourceParameters(session, self.cfg.url)
-        self._ctx = Context(self.session, self.cfg.url)
+        self._ctx = Context(self)
 
     @property
     def version(self) -> str | None:
@@ -180,7 +180,7 @@ def me(self) -> User:
         User
             The currently authenticated user.
         """
-        return me.get(self.resource_params)
+        return me.get(self._ctx)
 
     @property
     def groups(self) -> Groups:
@@ -191,7 +191,7 @@ def groups(self) -> Groups:
         Groups
             The groups resource interface.
         """
-        return Groups(self.resource_params)
+        return Groups(self._ctx)
 
     @property
     def tasks(self) -> Tasks:
@@ -215,7 +215,7 @@ def users(self) -> Users:
         Users
             The users resource instance.
         """
-        return Users(self.resource_params)
+        return Users(self._ctx)
 
     @property
     def content(self) -> Content:
@@ -227,7 +227,7 @@ def content(self) -> Content:
         Content
             The content resource instance.
         """
-        return Content(self.resource_params)
+        return Content(self._ctx)
 
     @property
     def metrics(self) -> Metrics:
diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py
index 404867b7..00407049 100644
--- a/src/posit/connect/content.py
+++ b/src/posit/connect/content.py
@@ -20,7 +20,6 @@
 from . import tasks
 from ._api import ApiDictEndpoint, JsonifiableDict
 from .bundles import Bundles
-from .context import Context
 from .env import EnvVars
 from .errors import ClientError
 from .jobs import JobsMixin
@@ -32,6 +31,7 @@
 from .variants import Variants
 
 if TYPE_CHECKING:
+    from .context import Context
     from .tasks import Task
 
 
@@ -221,30 +221,32 @@ class _AttrsCreate(_AttrsBase):
     @overload
     def __init__(
         self,
+        ctx: Context,
         /,
-        params: ResourceParameters,
+        *,
         guid: str,
     ) -> None: ...
 
     @overload
     def __init__(
         self,
+        ctx: Context,
         /,
-        params: ResourceParameters,
+        *,
         guid: str,
         **kwargs: Unpack[ContentItem._Attrs],
     ) -> None: ...
 
     def __init__(
         self,
+        ctx: Context,
         /,
-        params: ResourceParameters,
+        *,
         guid: str,
         **kwargs: Unpack[ContentItem._AttrsNotRequired],
     ) -> None:
         _assert_guid(guid)
 
-        ctx = Context(params.session, params.url)
         path = f"v1/content/{guid}"
         super().__init__(ctx, path, guid=guid, **kwargs)
 
@@ -291,8 +293,8 @@ def create_repository(
     def delete(self) -> None:
         """Delete the content item."""
         path = f"v1/content/{self['guid']}"
-        url = self.params.url + path
-        self.params.session.delete(url)
+        url = self._ctx.url + path
+        self._ctx.session.delete(url)
 
     def deploy(self) -> tasks.Task:
         """Deploy the content.
@@ -311,8 +313,8 @@ def deploy(self) -> tasks.Task:
         None
         """
         path = f"v1/content/{self['guid']}/deploy"
-        url = self.params.url + path
-        response = self.params.session.post(url, json={"bundle_id": None})
+        url = self._ctx.url + path
+        response = self._ctx.session.post(url, json={"bundle_id": None})
         result = response.json()
         ts = tasks.Tasks(self.params)
         return ts.get(result["task_id"])
@@ -367,8 +369,8 @@ def restart(self) -> None:
             self.environment_variables.create(key, unix_epoch_in_seconds)
             self.environment_variables.delete(key)
             # GET via the base Connect URL to force create a new worker thread.
-            url = posixpath.join(dirname(self.params.url), f"content/{self['guid']}")
-            self.params.session.get(url)
+            url = posixpath.join(dirname(self._ctx.url), f"content/{self['guid']}")
+            self._ctx.session.get(url)
             return None
         else:
             raise ValueError(
@@ -438,8 +440,8 @@ def update(
         -------
         None
         """
-        url = self.params.url + f"v1/content/{self['guid']}"
-        response = self.params.session.patch(url, json=attrs)
+        url = self._ctx.url + f"v1/content/{self['guid']}"
+        response = self._ctx.session.patch(url, json=attrs)
         super().update(**response.json())
 
     # Relationships
@@ -464,7 +466,9 @@ def owner(self) -> dict:
             # If it's not included, we can retrieve the information by `owner_guid`
             from .users import Users
 
-            self["owner"] = Users(self.params).get(self["owner_guid"])
+            self["owner"] = Users(
+                self._ctx,
+            ).get(self["owner_guid"])
         return self["owner"]
 
     @property
@@ -512,12 +516,13 @@ class Content(Resources):
 
     def __init__(
         self,
-        params: ResourceParameters,
+        ctx: Context,
         *,
         owner_guid: str | None = None,
     ) -> None:
-        super().__init__(params)
+        super().__init__(ctx.client.resource_params)
         self.owner_guid = owner_guid
+        self._ctx = ctx
 
     def count(self) -> int:
         """Count the number of content items.
@@ -592,9 +597,9 @@ def create(
         ContentItem
         """
         path = "v1/content"
-        url = self.params.url + path
-        response = self.params.session.post(url, json=attrs)
-        return ContentItem(self.params, **response.json())
+        url = self._ctx.url + path
+        response = self._ctx.session.post(url, json=attrs)
+        return ContentItem(self._ctx, **response.json())
 
     @overload
     def find(
@@ -680,11 +685,11 @@ def find(self, include: Optional[str | list[Any]] = None, **conditions) -> List[
             conditions["owner_guid"] = self.owner_guid
 
         path = "v1/content"
-        url = self.params.url + path
-        response = self.params.session.get(url, params=conditions)
+        url = self._ctx.url + path
+        response = self._ctx.session.get(url, params=conditions)
         return [
             ContentItem(
-                self.params,
+                self._ctx,
                 **result,
             )
             for result in response.json()
@@ -853,6 +858,6 @@ def get(self, guid: str) -> ContentItem:
         ContentItem
         """
         path = f"v1/content/{guid}"
-        url = self.params.url + path
-        response = self.params.session.get(url)
-        return ContentItem(self.params, **response.json())
+        url = self._ctx.url + path
+        response = self._ctx.session.get(url)
+        return ContentItem(self._ctx, **response.json())
diff --git a/src/posit/connect/context.py b/src/posit/connect/context.py
index 1e275838..6faaf545 100644
--- a/src/posit/connect/context.py
+++ b/src/posit/connect/context.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import functools
+import weakref
 from typing import TYPE_CHECKING, Protocol
 
 from packaging.version import Version
@@ -8,6 +9,7 @@
 if TYPE_CHECKING:
     import requests
 
+    from .client import Client
     from .urls import Url
 
 
@@ -28,9 +30,12 @@ def wrapper(instance: ContextManager, *args, **kwargs):
 
 
 class Context:
-    def __init__(self, session: requests.Session, url: Url):
-        self.session = session
-        self.url = url
+    def __init__(self, client: Client):
+        self.session: requests.Session = client.session
+        self.url: Url = client.cfg.url
+        # Since this is a child object of the client, we use a weak reference to avoid circular
+        # references (which would prevent garbage collection)
+        self.client: Client = weakref.proxy(client)
 
     @property
     def version(self) -> str | None:
diff --git a/src/posit/connect/groups.py b/src/posit/connect/groups.py
index 217f47ee..2a909c5f 100644
--- a/src/posit/connect/groups.py
+++ b/src/posit/connect/groups.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, List, overload
+from typing import TYPE_CHECKING, List, Optional, overload
 
 from .paginator import Paginator
 from .resources import Resource, Resources
@@ -10,18 +10,264 @@
 if TYPE_CHECKING:
     import requests
 
+    from posit.connect.context import Context
+
+    from .users import User
+
 
 class Group(Resource):
+    def __init__(self, ctx: Context, **kwargs) -> None:
+        super().__init__(ctx.client.resource_params, **kwargs)
+        self._ctx: Context = ctx
+
+    @property
+    def members(self) -> GroupMembers:
+        """Get the group members.
+
+        Returns
+        -------
+        GroupMembers
+            All the users in the group.
+
+        Examples
+        --------
+        ```python
+        from posit.connect import Client
+
+        client = Client("https://posit.example.com", "API_KEY")
+
+        group = client.groups.get("GROUP_GUID_HERE")
+        group_users = group.members.find()
+
+        # Get count of group members
+        group_user_count = group.members.count()
+        ```
+
+        """
+        return GroupMembers(self._ctx, group_guid=self["guid"])
+
     def delete(self) -> None:
-        """Delete the group."""
+        """Delete the group.
+
+        Examples
+        --------
+        ```python
+        from posit.connect import Client
+
+        client = Client("https://posit.example.com", "API_KEY")
+
+        group = client.groups.get("GROUP_GUID_HERE")
+
+        # Delete the group
+        group.delete()
+        ```
+        """
         path = f"v1/groups/{self['guid']}"
-        url = self.params.url + path
-        self.params.session.delete(url)
+        url = self._ctx.url + path
+        self._ctx.session.delete(url)
+
+
+class GroupMembers(Resources):
+    def __init__(self, ctx: Context, group_guid: str) -> None:
+        super().__init__(ctx.client.resource_params)
+        self._group_guid = group_guid
+        self._ctx: Context = ctx
+
+    @overload
+    def add(self, user: User, /) -> None: ...
+    @overload
+    def add(self, /, *, user_guid: str) -> None: ...
+
+    def add(self, user: Optional[User] = None, /, *, user_guid: Optional[str] = None) -> None:
+        """Add a user to the group.
+
+        Parameters
+        ----------
+        user : User
+            User object to add to the group. Only one of `user=` or `user_guid=` can be provided.
+        user_guid : str
+            The user GUID.
+
+        Examples
+        --------
+        ```python
+        from posit.connect import Client
+
+        client = Client("https://posit.example.com", "API_KEY")
+
+        group = client.groups.get("GROUP_GUID_HERE")
+        user = client.users.get("USER_GUID_HERE")
+
+        # Add a user to the group
+        group.members.add(user)
+
+        # Add multiple users to the group
+        users = client.users.find()
+        for user in users:
+            group.members.add(user)
+
+        # Add a user to the group by GUID
+        group.members.add(user_guid="USER_GUID_HERE")
+        ```
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#post-/v1/groups/-group_guid-/members
+        """
+        if user is not None:
+            from .users import User
+
+            if user_guid:
+                raise ValueError("Only one of `user=` or `user_guid=` should be provided.")
+            if not isinstance(user, User):
+                raise TypeError(f"`user=` is not a `User` object. Received {user}")
+
+            user_guid = user["guid"]
+
+        if not isinstance(user_guid, str):
+            raise TypeError(f"`user_guid=` should be a string. Received {user_guid}")
+        if not user_guid:
+            raise ValueError("`user_guid=` should not be empty.")
+
+        path = f"v1/groups/{self._group_guid}/members"
+        url = self._ctx.url + path
+        self._ctx.session.post(url, json={"user_guid": user_guid})
+
+    @overload
+    def delete(self, user: User, /) -> None: ...
+    @overload
+    def delete(self, /, *, user_guid: str) -> None: ...
+
+    def delete(self, user: Optional[User] = None, /, *, user_guid: Optional[str] = None) -> None:
+        """Remove a user from the group.
+
+        Parameters
+        ----------
+        user : User
+            User object to add to the group. Only one of `user=` or `user_guid=` can be provided.
+        user_guid : str
+            The user GUID.
+
+        Examples
+        --------
+        ```python
+        from posit.connect import Client
+
+        client = Client("https://posit.example.com", "API_KEY")
+
+        group = client.groups.get("GROUP_GUID_HERE")
+
+        # Remove a user from the group
+        first_user = group.members.find()[0]
+        group.members.delete(first_user)
+
+        # Remove multiple users from the group
+        group_users = group.members.find()[:2]
+        for group_user in group_users:
+            group.members.delete(group_user)
+
+        # Remove a user from the group by GUID
+        group.members.delete(user_guid="USER_GUID_HERE")
+        ```
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#delete-/v1/groups/-group_guid-/members/-user_guid-
+        """
+        if user is not None:
+            from .users import User
+
+            if user_guid:
+                raise ValueError("Only one of `user=` or `user_guid=` should be provided.")
+            if not isinstance(user, User):
+                raise TypeError(f"`user=` is not a `User` object. Received {user}")
+
+            user_guid = user["guid"]
+
+        if not isinstance(user_guid, str):
+            raise TypeError(f"`user_guid=` should be a string. Received {user_guid}")
+        if not user_guid:
+            raise ValueError("`user_guid=` should not be empty.")
+
+        path = f"v1/groups/{self._group_guid}/members/{user_guid}"
+        url = self._ctx.url + path
+        self._ctx.session.delete(url)
+
+    def find(self) -> list[User]:
+        """Find group members.
+
+        Returns
+        -------
+        list[User]
+            All the users in the group.
+
+        Examples
+        --------
+        ```python
+        from posit.connect import Client
+
+        client = Client("https://posit.example.com", "API_KEY")
+
+        group = client.groups.get("GROUP_GUID_HERE")
+
+        # Find all users in the group
+        group_users = group.members.find()
+        ```
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/groups/-group_guid-/members
+        """
+        # Avoid circular import
+        from .users import User
+
+        path = f"v1/groups/{self._group_guid}/members"
+        url = self._ctx.url + path
+        paginator = Paginator(self._ctx.session, url)
+        member_dicts = paginator.fetch_results()
+
+        # For each member in the group
+        users = [User(self._ctx, **member_dict) for member_dict in member_dicts]
+        return users
+
+    def count(self) -> int:
+        """Count the number of group members.
+
+        Returns
+        -------
+        int
+
+        Examples
+        --------
+        ```python
+        from posit.connect import Client
+
+        client = Client("https://posit.example.com", "API_KEY")
+
+        group = client.groups.get("GROUP_GUID_HERE")
+
+        # Get count of group members
+        group_user_count = group.members.count()
+        ```
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/groups/-group_guid-/members
+        """
+        path = f"v1/groups/{self._group_guid}/members"
+        url = self._ctx.url + path
+        response = self._ctx.session.get(url, params={"page_size": 1})
+        result = response.json()
+        return result["total"]
 
 
 class Groups(Resources):
     """Groups resource."""
 
+    def __init__(self, ctx: Context) -> None:
+        super().__init__(ctx.client.resource_params)
+        self._ctx: Context = ctx
+
     @overload
     def create(self, *, name: str, unique_id: str | None) -> Group:
         """Create a group.
@@ -34,6 +280,10 @@ def create(self, *, name: str, unique_id: str | None) -> Group:
         Returns
         -------
         Group
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#post-/v1/groups
         """
 
     @overload
@@ -58,9 +308,9 @@ def create(self, **kwargs) -> Group:
         Group
         """
         path = "v1/groups"
-        url = self.params.url + path
-        response = self.params.session.post(url, json=kwargs)
-        return Group(self.params, **response.json())
+        url = self._ctx.url + path
+        response = self._ctx.session.post(url, json=kwargs)
+        return Group(self._ctx, **response.json())
 
     @overload
     def find(
@@ -83,14 +333,18 @@ def find(self, **kwargs):
         Returns
         -------
         List[Group]
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/groups
         """
         path = "v1/groups"
-        url = self.params.url + path
-        paginator = Paginator(self.params.session, url, params=kwargs)
+        url = self._ctx.url + path
+        paginator = Paginator(self._ctx.session, url, params=kwargs)
         results = paginator.fetch_results()
         return [
             Group(
-                self.params,
+                self._ctx,
                 **result,
             )
             for result in results
@@ -117,15 +371,19 @@ def find_one(self, **kwargs) -> Group | None:
         Returns
         -------
         Group | None
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/groups
         """
         path = "v1/groups"
-        url = self.params.url + path
-        paginator = Paginator(self.params.session, url, params=kwargs)
+        url = self._ctx.url + path
+        paginator = Paginator(self._ctx.session, url, params=kwargs)
         pages = paginator.fetch_pages()
         results = (result for page in pages for result in page.results)
         groups = (
             Group(
-                self.params,
+                self._ctx,
                 **result,
             )
             for result in results
@@ -142,11 +400,15 @@ def get(self, guid: str) -> Group:
         Returns
         -------
         Group
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/groups
         """
-        url = self.params.url + f"v1/groups/{guid}"
-        response = self.params.session.get(url)
+        url = self._ctx.url + f"v1/groups/{guid}"
+        response = self._ctx.session.get(url)
         return Group(
-            self.params,
+            self._ctx,
             **response.json(),
         )
 
@@ -156,9 +418,13 @@ def count(self) -> int:
         Returns
         -------
         int
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/groups
         """
         path = "v1/groups"
-        url = self.params.url + path
-        response: requests.Response = self.params.session.get(url, params={"page_size": 1})
+        url = self._ctx.url + path
+        response: requests.Response = self._ctx.session.get(url, params={"page_size": 1})
         result: dict = response.json()
         return result["total"]
diff --git a/src/posit/connect/me.py b/src/posit/connect/me.py
index a32d7c63..ee795724 100644
--- a/src/posit/connect/me.py
+++ b/src/posit/connect/me.py
@@ -1,9 +1,8 @@
-from posit.connect.resources import ResourceParameters
-
+from .context import Context
 from .users import User
 
 
-def get(params: ResourceParameters) -> User:
+def get(ctx: Context) -> User:
     """
     Gets the current user.
 
@@ -15,6 +14,6 @@ def get(params: ResourceParameters) -> User:
     -------
         User: The current user.
     """
-    url = params.url + "v1/user"
-    response = params.session.get(url)
-    return User(params, **response.json())
+    url = ctx.url + "v1/user"
+    response = ctx.session.get(url)
+    return User(ctx, **response.json())
diff --git a/src/posit/connect/permissions.py b/src/posit/connect/permissions.py
index 5806e019..a20b6f3f 100644
--- a/src/posit/connect/permissions.py
+++ b/src/posit/connect/permissions.py
@@ -111,8 +111,15 @@ def find(self, **kwargs) -> List[Permission]:
         """
         path = f"v1/content/{self.content_guid}/permissions"
         url = self.params.url + path
-        response = self.params.session.get(url, json=kwargs)
+        response = self.params.session.get(url)
+        kwargs_items = kwargs.items()
         results = response.json()
+        if len(kwargs_items) > 0:
+            results = [
+                result
+                for result in results
+                if isinstance(result, dict) and (result.items() >= kwargs_items)
+            ]
         return [Permission(self.params, **result) for result in results]
 
     def find_one(self, **kwargs) -> Permission | None:
@@ -142,25 +149,18 @@ def get(self, uid: str) -> Permission:
         response = self.params.session.get(url)
         return Permission(self.params, **response.json())
 
-    def destroy(self, *permissions: str | Group | User | Permission) -> list[Permission]:
-        """Remove supplied content item permissions.
+    def destroy(self, permission: str | Group | User | Permission, /) -> None:
+        """Remove supplied content item permission.
 
-        Removes all provided permissions from the content item's permissions. If a permission isn't
-        found, it is silently ignored.
+        Removes provided permission from the content item's permissions.
 
         Parameters
         ----------
-        *permissions : str | Group | User | Permission
-            The content item permissions to remove. If a `str` is received, it is compared against
+        permission : str | Group | User | Permission
+            The content item permission to remove. If a `str` is received, it is compared against
             the `Permissions`'s `principal_guid`. If a `Group` or `User` is received, the associated
             `Permission` will be removed.
 
-        Returns
-        -------
-        list[Permission]
-            The removed permissions. If a permission is not found, there is nothing to remove and 
-            it is not included in the returned list.
-
         Examples
         --------
         ```python
@@ -175,51 +175,46 @@ def destroy(self, *permissions: str | Group | User | Permission) -> list[Permiss
         ############################
 
         client = connect.Client()
+        content_item = client.content.get(content_guid)
 
         # Remove a single permission by principal_guid
-        client.content.get(content_guid).permissions.destroy(principal_guid)
+        content_item.permissions.destroy(principal_guid)
 
         # Remove by user (if principal_guid is a user)
         user = client.users.get(principal_guid)
-        client.content.get(content_guid).permissions.destroy(user)
+        content_item.permissions.destroy(user)
 
         # Remove by group (if principal_guid is a group)
         group = client.groups.get(principal_guid)
-        client.content.get(content_guid).permissions.destroy(group)
+        content_item.permissions.destroy(group)
 
         # Remove all groups with a matching prefix name
         groups = client.groups.find(prefix=group_name_prefix)
-        client.content.get(content_guid).permissions.destroy(*groups)
+        for group in groups:
+            content_item.permissions.destroy(group)
 
         # Confirm new permissions
-        client.content.get(content_guid).permissions.find()
+        content_item.permissions.find()
         ```
         """
         from .groups import Group
         from .users import User
 
-        if len(permissions) == 0:
-            raise ValueError("Expected at least one `permission` to remove")
-
-        principal_guids: set[str] = set()
-
-        for arg in permissions:
-            if isinstance(arg, str):
-                principal_guid = arg
-            elif isinstance(arg, (Group, User)):
-                principal_guid: str = arg["guid"]
-            elif isinstance(arg, Permission):
-                principal_guid: str = arg["principal_guid"]
-            else:
-                raise TypeError(
-                    f"destroy() expected argument type 'str', 'User', 'Group', or 'Permission' but got '{type(arg).__name__}'",
-                )
-            principal_guids.add(principal_guid)
-
-        destroyed_permissions: list[Permission] = []
-        for permission in self.find():
-            if permission["principal_guid"] in principal_guids:
-                permission.destroy()
-                destroyed_permissions.append(permission)
-
-        return destroyed_permissions
+        if isinstance(permission, str):
+            permission_obj = self.get(permission)
+        elif isinstance(permission, (Group, User)):
+            principal_guid: str = permission["guid"]
+            permission_obj = self.find_one(
+                principal_guid=principal_guid,
+            )
+            print("Barret!", permission, principal_guid, permission_obj)
+            if permission_obj is None:
+                raise ValueError(f"Permission with principal_guid '{principal_guid}' not found.")
+        elif isinstance(permission, Permission):
+            permission_obj = permission
+        else:
+            raise TypeError(
+                f"destroy() expected `permission=` to have type `str | User | Group | Permission`. Received `{permission}` of type `{type(permission)}`.",
+            )
+
+        permission_obj.destroy()
diff --git a/src/posit/connect/users.py b/src/posit/connect/users.py
index dd9c6833..ca99ced0 100644
--- a/src/posit/connect/users.py
+++ b/src/posit/connect/users.py
@@ -2,20 +2,29 @@
 
 from __future__ import annotations
 
-from typing import List, Literal
+from typing import TYPE_CHECKING, List, Literal
 
 from typing_extensions import NotRequired, Required, TypedDict, Unpack
 
 from . import me
 from .content import Content
 from .paginator import Paginator
-from .resources import Resource, ResourceParameters, Resources
+from .resources import Resource, Resources
+
+if TYPE_CHECKING:
+    from posit.connect.context import Context
+
+    from .groups import Group
 
 
 class User(Resource):
+    def __init__(self, ctx: Context, /, **attributes) -> None:
+        super().__init__(ctx.client.resource_params, **attributes)
+        self._ctx: Context = ctx
+
     @property
     def content(self) -> Content:
-        return Content(self.params, owner_guid=self["guid"])
+        return Content(self._ctx, owner_guid=self["guid"])
 
     def lock(self, *, force: bool = False):
         """
@@ -41,15 +50,19 @@ def lock(self, *, force: bool = False):
         Attempt to lock your own account (will raise `RuntimeError` unless `force` is set to `True`):
 
         >>> user.lock(force=True)
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#post-/v1/users/-guid-/lock
         """
-        _me = me.get(self.params)
+        _me = me.get(self._ctx)
         if _me["guid"] == self["guid"] and not force:
             raise RuntimeError(
                 "You cannot lock your own account. Set force=True to override this behavior.",
             )
-        url = self.params.url + f"v1/users/{self['guid']}/lock"
+        url = self._ctx.url + f"v1/users/{self['guid']}/lock"
         body = {"locked": True}
-        self.params.session.post(url, json=body)
+        self._ctx.session.post(url, json=body)
         super().update(locked=True)
 
     def unlock(self):
@@ -67,10 +80,14 @@ def unlock(self):
         Unlock a user's account:
 
         >>> user.unlock()
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#post-/v1/users/-guid-/lock
         """
-        url = self.params.url + f"v1/users/{self['guid']}/lock"
+        url = self._ctx.url + f"v1/users/{self['guid']}/lock"
         body = {"locked": False}
-        self.params.session.post(url, json=body)
+        self._ctx.session.post(url, json=body)
         super().update(locked=False)
 
     class UpdateUser(TypedDict):
@@ -115,17 +132,187 @@ def update(
         Update the user's first and last name:
 
         >>> user.update(first_name="Jane", last_name="Smith")
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#put-/v1/users/-guid-
         """
-        url = self.params.url + f"v1/users/{self['guid']}"
-        response = self.params.session.put(url, json=kwargs)
+        url = self._ctx.url + f"v1/users/{self['guid']}"
+        response = self._ctx.session.put(url, json=kwargs)
         super().update(**response.json())
 
+    @property
+    def groups(self) -> UserGroups:
+        """
+        Retrieve the groups to which the user belongs.
+
+        Returns
+        -------
+        UserGroups
+            Helper class that returns the groups of which the user is a member.
+
+        Examples
+        --------
+        Retrieve the groups to which the user belongs:
+
+        ```python
+        user = client.users.get("USER_GUID_HERE")
+        groups = user.groups.find()
+        ```
+        """
+        return UserGroups(self._ctx, self["guid"])
+
+
+class UserGroups(Resources):
+    def __init__(self, ctx: Context, user_guid: str) -> None:
+        super().__init__(ctx.client.resource_params)
+        self._ctx: Context = ctx
+        self._user_guid: str = user_guid
+
+    def add(self, group: str | Group) -> None:
+        """
+        Add the user to the specified group.
+
+        Parameters
+        ----------
+        group : str | Group
+            The group guid or `Group` object to which the user will be added.
+
+        Examples
+        --------
+        ```python
+        from posit.connect import Client
+
+        client = Client("https://posit.example.com", "API_KEY")
+
+        group = client.groups.get("GROUP_GUID_HERE")
+        user = client.users.get("USER_GUID_HERE")
+
+        # Add the user to the group
+        user.groups.add(group)
+
+        # Add the user to multiple groups
+        groups = [
+            client.groups.get("GROUP_GUID_1"),
+            client.groups.get("GROUP_GUID_2"),
+        ]
+        for group in groups:
+            user.groups.add(group)
+
+        # Add the user to a group by GUID
+        user.groups.add("GROUP_GUID_HERE")
+        ```
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#post-/v1/groups/-group_guid-/members
+        """
+        from .groups import Group
+
+        if isinstance(group, Group):
+            group.members.add(user_guid=self._user_guid)
+            return
+
+        if not isinstance(group, str):
+            raise TypeError(f"`group=` must be a `str | Group`. Received {group}")
+        if not group:
+            raise ValueError("`group=` must not be empty.")
+
+        group_obj = self._ctx.client.groups.get(group)
+        group_obj.members.add(user_guid=self._user_guid)
+
+    def delete(self, group: str | Group) -> None:
+        """
+        Remove the user from the specified group.
+
+        Parameters
+        ----------
+        group : str | Group
+            The group to which the user will be added.
+
+        Examples
+        --------
+        ```python
+        from posit.connect import Client
+
+        client = Client("https://posit.example.com", "API_KEY")
+
+        group = client.groups.get("GROUP_GUID_HERE")
+        user = client.users.get("USER_GUID_HERE")
+
+        # Remove the user from the group
+        user.groups.delete(group)
+
+        # Remove the user from multiple groups
+        groups = [
+            client.groups.get("GROUP_GUID_1"),
+            client.groups.get("GROUP_GUID_2"),
+        ]
+        for group in groups:
+            user.groups.delete(group)
+
+        # Remove the user from a group by GUID
+        user.groups.delete("GROUP_GUID_HERE")
+        ```
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#delete-/v1/groups/-group_guid-/members/-user_guid-
+        """
+        from .groups import Group
+
+        if isinstance(group, Group):
+            group.members.delete(user_guid=self._user_guid)
+            return
+
+        if not isinstance(group, str):
+            raise TypeError(f"`group=` must be a `str | Group`. Received {group}")
+        if not group:
+            raise ValueError("`group=` must not be empty.")
+
+        group_obj = self._ctx.client.groups.get(group)
+        group_obj.members.delete(user_guid=self._user_guid)
+
+    def find(self) -> List[Group]:
+        """
+        Retrieve the groups to which the user belongs.
+
+        Returns
+        -------
+        List[Group]
+            A list of groups to which the user belongs.
+
+        Examples
+        --------
+        ```python
+        from posit.connect import Client
+
+        client = Client("https://posit.example.com", "API_KEY")
+
+        user = client.users.get("USER_GUID_HERE")
+        groups = user.groups.find()
+        ```
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/groups/-group_guid-/members
+        """
+        self_groups: list[Group] = []
+        for group in self._ctx.client.groups.find():
+            group_users = group.members.find()
+            for group_user in group_users:
+                if group_user["guid"] == self._user_guid:
+                    self_groups.append(group)
+
+        return self_groups
+
 
 class Users(Resources):
     """Users resource."""
 
-    def __init__(self, params: ResourceParameters) -> None:
-        super().__init__(params)
+    def __init__(self, ctx: Context) -> None:
+        super().__init__(ctx.client.resource_params)
+        self._ctx: Context = ctx
 
     class CreateUser(TypedDict):
         """Create user request."""
@@ -195,11 +382,15 @@ def create(self, **attributes: Unpack[CreateUser]) -> User:
         ...     user_must_set_password=True,
         ...     user_role="viewer",
         ... )
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#post-/v1/users
         """
         # todo - use the 'context' module to inspect the 'authentication' object and route to POST (local) or PUT (remote).
-        url = self.params.url + "v1/users"
-        response = self.params.session.post(url, json=attributes)
-        return User(self.params, **response.json())
+        url = self._ctx.url + "v1/users"
+        response = self._ctx.session.post(url, json=attributes)
+        return User(self._ctx, **response.json())
 
     class FindUser(TypedDict):
         """Find user request."""
@@ -239,13 +430,17 @@ def find(self, **conditions: Unpack[FindUser]) -> List[User]:
         Find all users who are locked or licensed:
 
         >>> users = client.find(account_status="locked|licensed")
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/users
         """
-        url = self.params.url + "v1/users"
-        paginator = Paginator(self.params.session, url, params={**conditions})
+        url = self._ctx.url + "v1/users"
+        paginator = Paginator(self._ctx.session, url, params={**conditions})
         results = paginator.fetch_results()
         return [
             User(
-                self.params,
+                self._ctx,
                 **user,
             )
             for user in results
@@ -282,14 +477,18 @@ def find_one(self, **conditions: Unpack[FindUser]) -> User | None:
         Find a user who is locked or licensed:
 
         >>> user = client.find_one(account_status="locked|licensed")
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/users
         """
-        url = self.params.url + "v1/users"
-        paginator = Paginator(self.params.session, url, params={**conditions})
+        url = self._ctx.url + "v1/users"
+        paginator = Paginator(self._ctx.session, url, params={**conditions})
         pages = paginator.fetch_pages()
         results = (result for page in pages for result in page.results)
         users = (
             User(
-                self.params,
+                self._ctx,
                 **result,
             )
             for result in results
@@ -312,11 +511,15 @@ def get(self, uid: str) -> User:
         Examples
         --------
         >>> user = client.get("123e4567-e89b-12d3-a456-426614174000")
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/users
         """
-        url = self.params.url + f"v1/users/{uid}"
-        response = self.params.session.get(url)
+        url = self._ctx.url + f"v1/users/{uid}"
+        response = self._ctx.session.get(url)
         return User(
-            self.params,
+            self._ctx,
             **response.json(),
         )
 
@@ -327,8 +530,12 @@ def count(self) -> int:
         Returns
         -------
         int
+
+        See Also
+        --------
+        * https://docs.posit.co/connect/api/#get-/v1/users
         """
-        url = self.params.url + "v1/users"
-        response = self.params.session.get(url, params={"page_size": 1})
+        url = self._ctx.url + "v1/users"
+        response = self._ctx.session.get(url, params={"page_size": 1})
         result: dict = response.json()
         return result["total"]
diff --git a/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions.json b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions.json
index 9db6f6bf..67a27f92 100644
--- a/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions.json
+++ b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions.json
@@ -1,16 +1,16 @@
 [
-    {
-        "id": 94,
-        "content_guid": "f2f37341-e21d-3d80-c698-a935ad614066",
-        "principal_guid": "78974391-d89f-4f11-916a-ba50cfe993db",
-        "principal_type": "user",
-        "role": "owner"
-    },
-    {
-        "id": 59,
-        "content_guid": "f2f37341-e21d-3d80-c698-a935ad614066",
-        "principal_guid": "75b95fc0-ae02-4d85-8732-79a845143eed",
-        "principal_type": "group",
-        "role": "viewer"
-    }
+  {
+    "id": 94,
+    "content_guid": "f2f37341-e21d-3d80-c698-a935ad614066",
+    "principal_guid": "20a79ce3-6e87-4522-9faf-be24228800a4",
+    "principal_type": "user",
+    "role": "owner"
+  },
+  {
+    "id": 59,
+    "content_guid": "f2f37341-e21d-3d80-c698-a935ad614066",
+    "principal_guid": "6f300623-1e0c-48e6-a473-ddf630c0c0c3",
+    "principal_type": "group",
+    "role": "viewer"
+  }
 ]
diff --git a/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions/59.json b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions/59.json
new file mode 100644
index 00000000..2802e5a7
--- /dev/null
+++ b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions/59.json
@@ -0,0 +1,7 @@
+{
+  "id": 59,
+  "content_guid": "f2f37341-e21d-3d80-c698-a935ad614066",
+  "principal_guid": "6f300623-1e0c-48e6-a473-ddf630c0c0c3",
+  "principal_type": "group",
+  "role": "viewer"
+}
diff --git a/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions/94.json b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions/94.json
index 491db40c..f8d667d2 100644
--- a/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions/94.json
+++ b/tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/permissions/94.json
@@ -1,7 +1,7 @@
 {
-    "id": 94,
-    "content_guid": "f2f37341-e21d-3d80-c698-a935ad614066",
-    "principal_guid": "78974391-d89f-4f11-916a-ba50cfe993db",
-    "principal_type": "user",
-    "role": "owner"
+  "id": "94",
+  "content_guid": "f2f37341-e21d-3d80-c698-a935ad614066",
+  "principal_guid": "20a79ce3-6e87-4522-9faf-be24228800a4",
+  "principal_type": "user",
+  "role": "owner"
 }
diff --git a/tests/posit/connect/__api__/v1/groups.json b/tests/posit/connect/__api__/v1/groups.json
new file mode 100644
index 00000000..601d27ef
--- /dev/null
+++ b/tests/posit/connect/__api__/v1/groups.json
@@ -0,0 +1,16 @@
+{
+  "results": [
+    {
+      "guid": "empty-group-guid",
+      "name": "Empty Friends",
+      "owner_guid": "empty-owner-guid"
+    },
+    {
+      "guid": "6f300623-1e0c-48e6-a473-ddf630c0c0c3",
+      "name": "Friends",
+      "owner_guid": "20a79ce3-6e87-4522-9faf-be24228800a4"
+    }
+  ],
+  "current_page": 1,
+  "total": 2
+}
diff --git a/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json b/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json
index bcd0c7a2..44ee4a0d 100644
--- a/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json
+++ b/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json
@@ -1,5 +1,5 @@
 {
-    "guid": "6f300623-1e0c-48e6-a473-ddf630c0c0c3",
-    "name": "Friends",
-    "owner_guid": "20a79ce3-6e87-4522-9faf-be24228800a4"
-  }
+  "guid": "6f300623-1e0c-48e6-a473-ddf630c0c0c3",
+  "name": "Friends",
+  "owner_guid": "20a79ce3-6e87-4522-9faf-be24228800a4"
+}
diff --git a/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3/members.json b/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3/members.json
new file mode 100644
index 00000000..93841f3f
--- /dev/null
+++ b/tests/posit/connect/__api__/v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3/members.json
@@ -0,0 +1,32 @@
+{
+  "results": [
+    {
+      "email": "alice@connect.example",
+      "username": "al",
+      "first_name": "Alice",
+      "last_name": "User",
+      "user_role": "publisher",
+      "created_time": "2017-08-08T15:24:32Z",
+      "updated_time": "2023-03-02T20:25:06Z",
+      "active_time": "2018-05-09T16:58:45Z",
+      "confirmed": true,
+      "locked": false,
+      "guid": "a01792e3-2e67-402e-99af-be04a48da074"
+    },
+    {
+      "email": "bob@connect.example",
+      "username": "robert",
+      "first_name": "Bob",
+      "last_name": "Loblaw",
+      "user_role": "publisher",
+      "created_time": "2023-01-06T19:47:29Z",
+      "updated_time": "2023-05-05T19:08:45Z",
+      "active_time": "2023-05-05T20:29:11Z",
+      "confirmed": true,
+      "locked": false,
+      "guid": "87c12c08-11cd-4de1-8da3-12a7579c4998"
+    }
+  ],
+  "current_page": 1,
+  "total": 2
+}
diff --git a/tests/posit/connect/__api__/v1/groups/empty-group-guid/members.json b/tests/posit/connect/__api__/v1/groups/empty-group-guid/members.json
new file mode 100644
index 00000000..ee21cbe5
--- /dev/null
+++ b/tests/posit/connect/__api__/v1/groups/empty-group-guid/members.json
@@ -0,0 +1,5 @@
+{
+  "results": [],
+  "current_page": 1,
+  "total": 0
+}
diff --git a/tests/posit/connect/__api__/v1/groups/groups.json b/tests/posit/connect/__api__/v1/groups/groups.json
new file mode 100644
index 00000000..601d27ef
--- /dev/null
+++ b/tests/posit/connect/__api__/v1/groups/groups.json
@@ -0,0 +1,16 @@
+{
+  "results": [
+    {
+      "guid": "empty-group-guid",
+      "name": "Empty Friends",
+      "owner_guid": "empty-owner-guid"
+    },
+    {
+      "guid": "6f300623-1e0c-48e6-a473-ddf630c0c0c3",
+      "name": "Friends",
+      "owner_guid": "20a79ce3-6e87-4522-9faf-be24228800a4"
+    }
+  ],
+  "current_page": 1,
+  "total": 2
+}
diff --git a/tests/posit/connect/test_client.py b/tests/posit/connect/test_client.py
index 5a8d1b42..3f7e8284 100644
--- a/tests/posit/connect/test_client.py
+++ b/tests/posit/connect/test_client.py
@@ -84,7 +84,12 @@ def test_init(
         MockConfig.assert_called_once_with(api_key=api_key, url=url)
         MockSession.assert_called_once()
 
-    def test__del__(self, MockAuth, MockConfig, MockSession):
+    def test__del__(
+        self,
+        MockAuth: MagicMock,
+        MockConfig: MagicMock,
+        MockSession: MagicMock,
+    ):
         api_key = "12345"
         url = "https://connect.example.com"
         client = Client(api_key=api_key, url=url)
diff --git a/tests/posit/connect/test_content.py b/tests/posit/connect/test_content.py
index 926df753..f18b46f5 100644
--- a/tests/posit/connect/test_content.py
+++ b/tests/posit/connect/test_content.py
@@ -1,5 +1,4 @@
 import pytest
-import requests
 import responses
 from responses import matchers
 
@@ -7,7 +6,6 @@
 from posit.connect.content import ContentItem, ContentItemRepository
 from posit.connect.context import Context
 from posit.connect.resources import ResourceParameters
-from posit.connect.urls import Url
 
 from .api import load_mock, load_mock_dict
 
@@ -562,15 +560,19 @@ def content_guid(self):
 
     @property
     def content_item(self):
-        return ContentItem(self.params, guid=self.content_guid)
+        return ContentItem(self.ctx, guid=self.content_guid)
 
     @property
     def endpoint(self):
         return f"{self.base_url}/__api__/v1/content/{self.content_guid}/repository"
 
+    @property
+    def client(self):
+        return Client(self.base_url, "12345")
+
     @property
     def ctx(self):
-        return Context(requests.Session(), Url(self.base_url))
+        return Context(self.client)
 
     @property
     def params(self):
diff --git a/tests/posit/connect/test_context.py b/tests/posit/connect/test_context.py
index be0330e4..c764effe 100644
--- a/tests/posit/connect/test_context.py
+++ b/tests/posit/connect/test_context.py
@@ -2,11 +2,10 @@
 from unittest.mock import MagicMock, Mock
 
 import pytest
-import requests
 import responses
 
+from posit.connect.client import Client
 from posit.connect.context import Context, requires
-from posit.connect.urls import Url
 
 
 class TestRequires:
@@ -65,9 +64,7 @@ def test_unknown(self):
             json={},
         )
 
-        session = requests.Session()
-        url = Url("http://connect.example")
-        ctx = Context(session, url)
+        ctx = Context(Client("http://connect.example", "12345"))
 
         assert ctx.version is None
 
@@ -78,13 +75,11 @@ def test_known(self):
             json={"version": "2024.09.24"},
         )
 
-        session = requests.Session()
-        url = Url("http://connect.example")
-        ctx = Context(session, url)
+        ctx = Context(Client("http://connect.example", "12345"))
 
         assert ctx.version == "2024.09.24"
 
     def test_setter(self):
-        ctx = Context(Mock(), Mock())
+        ctx = Context(Mock())
         ctx.version = "2024.09.24"
         assert ctx.version == "2024.09.24"
diff --git a/tests/posit/connect/test_groups.py b/tests/posit/connect/test_groups.py
index 71954e56..5a3b0b5b 100644
--- a/tests/posit/connect/test_groups.py
+++ b/tests/posit/connect/test_groups.py
@@ -1,7 +1,13 @@
 from unittest import mock
 from unittest.mock import Mock
 
+import pytest
+import responses
+
+from posit.connect.client import Client
+from posit.connect.context import Context
 from posit.connect.groups import Group
+from posit.connect.users import User
 
 from .api import load_mock_dict
 
@@ -24,3 +30,87 @@ def test_name(self):
 
     def test_owner_guid(self):
         assert self.item["owner_guid"] == "20a79ce3-6e87-4522-9faf-be24228800a4"
+
+
+class TestGroupMembers:
+    @classmethod
+    def setup_class(cls):
+        cls.client = Client("https://connect.example", "12345")
+        guid = "6f300623-1e0c-48e6-a473-ddf630c0c0c3"
+        fake_item = load_mock_dict(f"v1/groups/{guid}.json")
+        ctx = Context(cls.client)
+        cls.group = Group(ctx, **fake_item)
+
+    @responses.activate
+    def test_members_count(self):
+        responses.get(
+            f"https://connect.example/__api__/v1/groups/{self.group['guid']}/members",
+            json=load_mock_dict(f"v1/groups/{self.group['guid']}/members.json"),
+        )
+        group_members = self.group.members
+
+        assert group_members.count() == 2
+
+    @responses.activate
+    def test_members_find(self):
+        responses.get(
+            f"https://connect.example/__api__/v1/groups/{self.group['guid']}/members",
+            json=load_mock_dict(f"v1/groups/{self.group['guid']}/members.json"),
+        )
+
+        group_users = self.group.members.find()
+        assert len(group_users) == 2
+        for user in group_users:
+            assert isinstance(user, User)
+
+    @responses.activate
+    def test_members_add(self):
+        user_guid = "user-guid"
+        responses.post(
+            f"https://connect.example/__api__/v1/groups/{self.group['guid']}/members",
+            json=[],  # No need to return anything
+        )
+
+        user = User(self.client._ctx, guid=user_guid)
+        self.group.members.add(user)
+        self.group.members.add(user_guid=user["guid"])
+
+        with pytest.raises(TypeError):
+            self.group.members.add(
+                "not-a-user",  # pyright: ignore[reportArgumentType]
+            )
+        with pytest.raises(TypeError):
+            self.group.members.add(group_guid=42)  # pyright: ignore[reportCallIssue]
+        with pytest.raises(ValueError):
+            self.group.members.add(user, user_guid=user["guid"])  # pyright: ignore[reportCallIssue]
+        with pytest.raises(ValueError):
+            self.group.members.add(user_guid="")
+
+    @responses.activate
+    def test_members_delete(self):
+        user_guid = "user-guid"
+        responses.get(
+            f"https://connect.example/__api__/v1/groups/{self.group['guid']}",
+            json=dict(self.group),
+        )
+        responses.delete(
+            f"https://connect.example/__api__/v1/groups/{self.group['guid']}/members/{user_guid}",
+            json=[],  # No need to return anything
+        )
+
+        user = User(self.client._ctx, guid=user_guid)
+
+        self.group.members.delete(user)
+        self.group.members.delete(user_guid=user["guid"])
+
+        with pytest.raises(TypeError):
+            self.group.members.delete(
+                "not-a-user",  # pyright: ignore[reportArgumentType]
+            )
+        with pytest.raises(TypeError):
+            self.group.members.delete(group_guid=42)  # pyright: ignore[reportCallIssue]
+        with pytest.raises(ValueError):
+            self.group.members.delete(user, user_guid=user["guid"])  # pyright: ignore[reportCallIssue]
+
+        with pytest.raises(ValueError):
+            self.group.members.delete(user_guid="")
diff --git a/tests/posit/connect/test_permissions.py b/tests/posit/connect/test_permissions.py
index c8cc4b6a..dc0fa470 100644
--- a/tests/posit/connect/test_permissions.py
+++ b/tests/posit/connect/test_permissions.py
@@ -6,6 +6,8 @@
 import responses
 from responses import matchers
 
+from posit.connect.client import Client
+from posit.connect.context import Context
 from posit.connect.groups import Group
 from posit.connect.permissions import Permission, Permissions
 from posit.connect.resources import ResourceParameters
@@ -271,14 +273,19 @@ class TestPermissionsDestroy:
     @responses.activate
     def test_destroy(self):
         # data
-        permission_uid = "94"
         content_guid = "f2f37341-e21d-3d80-c698-a935ad614066"
         fake_permissions = load_mock_list(f"v1/content/{content_guid}/permissions.json")
-        fake_followup_permissions = fake_permissions.copy()
-        fake_followup_permissions.pop(0)
-        fake_permission = load_mock_dict(
-            f"v1/content/{content_guid}/permissions/{permission_uid}.json"
+
+        assert fake_permissions[0]["principal_type"] == "user"
+        user_permission_id = fake_permissions[0]["id"]
+        assert fake_permissions[1]["principal_type"] == "group"
+        group_permission_id = fake_permissions[1]["id"]
+        assert user_permission_id != group_permission_id
+
+        fake_manual_user_permission = load_mock_dict(
+            f"v1/content/{content_guid}/permissions/{user_permission_id}.json"
         )
+
         fake_user = load_mock_dict("v1/user.json")
         fake_group = load_mock_dict("v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json")
 
@@ -286,59 +293,58 @@ def test_destroy(self):
 
         # Used in internal for-loop
         mock_permissions_get = [
+            # Used to find all permissions when searching for user and group
             responses.get(
                 f"https://connect.example/__api__/v1/content/{content_guid}/permissions",
                 json=fake_permissions,
             ),
+            # Retrieve permissions object for permission id
             responses.get(
-                f"https://connect.example/__api__/v1/content/{content_guid}/permissions",
-                json=fake_followup_permissions,
+                f"https://connect.example/__api__/v1/content/{content_guid}/permissions/{user_permission_id}",
+                json=fake_manual_user_permission,
             ),
         ]
         # permission delete
-        mock_permission_delete = responses.delete(
-            f"https://connect.example/__api__/v1/content/{content_guid}/permissions/{permission_uid}",
-        )
+        mock_permission_deletes = [
+            responses.delete(
+                f"https://connect.example/__api__/v1/content/{content_guid}/permissions/{user_permission_id}",
+            ),
+            responses.delete(
+                f"https://connect.example/__api__/v1/content/{content_guid}/permissions/{group_permission_id}",
+            ),
+        ]
 
         # setup
-        params = ResourceParameters(requests.Session(), Url("https://connect.example/__api__"))
-        permissions = Permissions(params, content_guid=content_guid)
+        c = Client(api_key="12345", url="https://connect.example/")
+        ctx = Context(c)
+        permissions = Permissions(ctx.client.resource_params, content_guid=content_guid)
 
         # (Doesn't match any permissions, but that's okay)
-        user_to_remove = User(params, **fake_user)
-        group_to_remove = Group(params, **fake_group)
-        permission_to_remove = Permission(params, **fake_permission)
+        user_to_remove = User(ctx, **fake_user)
+        group_to_remove = Group(ctx, **fake_group)
+        permission_to_remove = Permission(
+            ctx.client.resource_params, **fake_manual_user_permission
+        )
 
         # invoke
-        destroyed_permission = permissions.destroy(
-            fake_permission["principal_guid"],
-            # Make sure duplicates are dropped
-            fake_permission["principal_guid"],
-            # Extract info from User, Group, Permission
-            user_to_remove,
-            group_to_remove,
-            permission_to_remove,
-        )
+        permissions.destroy(permission_to_remove["id"])
+        permissions.destroy(permission_to_remove)
+        permissions.destroy(user_to_remove)
+
+        permissions.destroy(group_to_remove)
+
+        # Assert values
+        assert mock_permissions_get[0].call_count == 2
+        assert mock_permissions_get[1].call_count == 1
+
+        # permission_id, user -> permission_id, permission -> permission_id
+        assert mock_permission_deletes[0].call_count == 3
+        # group -> permission_id
+        assert mock_permission_deletes[1].call_count == 1
+        # assert mock_permission_deletes[2].call_count == 1
 
         # Assert bad input value
         with pytest.raises(TypeError):
             permissions.destroy(
                 42  # pyright: ignore[reportArgumentType]
             )
-        with pytest.raises(ValueError):
-            permissions.destroy()
-
-        # Assert values
-        assert mock_permissions_get[0].call_count == 1
-        assert mock_permissions_get[1].call_count == 0
-        assert mock_permission_delete.call_count == 1
-        assert len(destroyed_permission) == 1
-        assert destroyed_permission[0] == fake_permission
-
-        # Invoking again is a no-op
-        destroyed_permission = permissions.destroy(fake_permission["principal_guid"])
-
-        assert mock_permissions_get[0].call_count == 1
-        assert mock_permissions_get[1].call_count == 1
-        assert mock_permission_delete.call_count == 1
-        assert len(destroyed_permission) == 0
diff --git a/tests/posit/connect/test_users.py b/tests/posit/connect/test_users.py
index 68adbb3b..d101b8ab 100644
--- a/tests/posit/connect/test_users.py
+++ b/tests/posit/connect/test_users.py
@@ -6,8 +6,10 @@
 from responses import matchers
 
 from posit.connect.client import Client
+from posit.connect.groups import Group
+from posit.connect.users import User
 
-from .api import load_mock
+from .api import load_mock, load_mock_dict
 
 session = Mock()
 url = Mock()
@@ -134,6 +136,104 @@ def test_unlock(self):
         assert not user["locked"]
 
 
+class TestUserGroups:
+    @responses.activate
+    def test_groups(self):
+        # Get user
+        carlos_guid = "20a79ce3-6e87-4522-9faf-be24228800a4"
+        carlos_empty_group_guid = "empty-group-guid"
+        responses.get(
+            f"https://connect.example/__api__/v1/users/{carlos_guid}",
+            json=load_mock_dict(f"v1/users/{carlos_guid}.json"),
+        )
+        responses.get(
+            f"https://connect.example/__api__/v1/groups/{carlos_empty_group_guid}/members?page_number=1&page_size=500",
+            json=load_mock_dict(f"v1/groups/{carlos_empty_group_guid}/members.json"),
+        )
+        responses.get(
+            "https://connect.example/__api__/v1/users",
+            json=load_mock_dict("v1/users?page_number=1&page_size=500.jsonc"),
+        )
+        # Get groups
+        responses.get(
+            "https://connect.example/__api__/v1/groups",
+            json=load_mock_dict("v1/groups.json"),
+        )
+        # Get group members
+        group_guid = "6f300623-1e0c-48e6-a473-ddf630c0c0c3"
+        responses.get(
+            f"https://connect.example/__api__/v1/groups/{group_guid}/members",
+            json=load_mock_dict(f"v1/groups/{group_guid}/members.json"),
+        )
+
+        client = Client("https://connect.example/", "12345")
+
+        carlos = client.users.get(carlos_guid)
+        no_groups = carlos.groups.find()
+        assert len(no_groups) == 0
+
+        user = client.users.find()[1]
+        assert user["guid"] == "87c12c08-11cd-4de1-8da3-12a7579c4998"
+
+        groups = user.groups.find()
+        assert isinstance(groups, list)
+        assert len(groups) == 1
+        group = groups[0]
+        assert isinstance(group, Group)
+        assert group["name"] == "Friends"
+
+    @responses.activate
+    def test_groups_add(self):
+        group_guid = "new-group-guid"
+        user_guid = "user-guid"
+        responses.get(
+            f"https://connect.example/__api__/v1/groups/{group_guid}",
+            json={"guid": group_guid},
+        )
+        responses.post(
+            f"https://connect.example/__api__/v1/groups/{group_guid}/members",
+            json=[],
+        )
+
+        client = Client("https://connect.example/", "12345")
+
+        user = User(client._ctx, guid=user_guid)
+        new_group = Group(client._ctx, guid=group_guid)
+        # Add via Group
+        user.groups.add(new_group)
+        # Add via guid
+        user.groups.add(new_group["guid"])
+
+        with pytest.raises(ValueError):
+            user.groups.add("")
+
+    @responses.activate
+    def test_groups_delete(self):
+        group_guid = "new-group-guid"
+        user_guid = "user-guid"
+        responses.get(
+            f"https://connect.example/__api__/v1/groups/{group_guid}",
+            json={"guid": group_guid},
+        )
+        responses.delete(
+            f"https://connect.example/__api__/v1/groups/{group_guid}/members/{user_guid}",
+            json=[],
+        )
+
+        client = Client("https://connect.example/", "12345")
+
+        user = User(client._ctx, guid=user_guid)
+        group = Group(client._ctx, guid=group_guid)
+
+        # Delete via Group
+        user.groups.delete(group)
+        # Delete via guid
+        user.groups.delete(group["guid"])
+
+        with pytest.raises(ValueError):
+            user.groups.delete("")
+
+
 class TestUsers:
     @responses.activate
     def test_users_get(self):