Repository: pypi/warehouse

Issue:
User who created the organization cannot leave the organization
Once a user has created an organization, they cannot leave the organization. 

To replicate this issue-

1. Click on 'Your organizations'
2. Click on 'Manage'
3. Scroll to the bottom of the page where the members of the organization are listed

The first owner cannot leave the organization.

![org1](https://user-images.githubusercontent.com/37237726/175095225-bf5c4cba-47ac-4346-8399-647a55066daa.png)





Discussion:
Priority 1/3. Need to enforce there is another owner. 

Resolving patch:
diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py
--- a/warehouse/manage/views.py
+++ b/warehouse/manage/views.py
@@ -1160,6 +1160,14 @@ def user_organizations(request):
         )
         .subquery()
     )
+    organizations_with_sole_owner = (
+        request.db.query(OrganizationRole.organization_id)
+        .join(organizations_owned)
+        .filter(OrganizationRole.role_name == "Owner")
+        .group_by(OrganizationRole.organization_id)
+        .having(func.count(OrganizationRole.organization_id) == 1)
+        .subquery()
+    )
     return {
         "organizations_owned": (
             request.db.query(Organization)
@@ -1179,6 +1187,15 @@ def user_organizations(request):
             .order_by(Organization.name)
             .all()
         ),
+        "organizations_with_sole_owner": (
+            request.db.query(Organization)
+            .join(
+                organizations_with_sole_owner,
+                Organization.id == organizations_with_sole_owner.c.organization_id,
+            )
+            .order_by(Organization.name)
+            .all()
+        ),
     }
 
 
@@ -2061,6 +2078,12 @@ def manage_organization_roles(
         "roles": roles,
         "invitations": invitations,
         "form": form,
+        "organizations_with_sole_owner": list(
+            organization.name
+            for organization in user_organizations(request)[
+                "organizations_with_sole_owner"
+            ]
+        ),
     }
 
 
@@ -2235,6 +2258,12 @@ def delete_organization_role(organization, request):
     organization_service = request.find_service(IOrganizationService, context=None)
     role_id = request.POST["role_id"]
     role = organization_service.get_organization_role(role_id)
+    organizations_sole_owned = set(
+        organization.id
+        for organization in user_organizations(request)["organizations_with_sole_owner"]
+    )
+    is_sole_owner = organization.id in organizations_sole_owned
+
     if not role or role.organization_id != organization.id:
         request.session.flash("Could not find member", queue="error")
     elif (
@@ -2243,8 +2272,12 @@ def delete_organization_role(organization, request):
         request.session.flash(
             "Cannot remove other people from the organization", queue="error"
         )
-    elif role.role_name == OrganizationRoleType.Owner and role.user == request.user:
-        request.session.flash("Cannot remove yourself as Owner", queue="error")
+    elif (
+        role.role_name == OrganizationRoleType.Owner
+        and role.user == request.user
+        and is_sole_owner
+    ):
+        request.session.flash("Cannot remove yourself as Sole Owner", queue="error")
     else:
         organization_service.delete_organization_role(role.id)
         organization.record_event(
@@ -4423,9 +4456,13 @@ def delete_project_role(project, request):
             .filter(Role.id == request.POST["role_id"])
             .one()
         )
+        projects_sole_owned = set(
+            project.name for project in user_projects(request)["projects_sole_owned"]
+        )
         removing_self = role.role_name == "Owner" and role.user == request.user
-        if removing_self:
-            request.session.flash("Cannot remove yourself as Owner", queue="error")
+        is_sole_owner = project.name in projects_sole_owned
+        if removing_self and is_sole_owner:
+            request.session.flash("Cannot remove yourself as Sole Owner", queue="error")
         else:
             request.db.delete(role)
             request.db.add(
@@ -4463,6 +4500,8 @@ def delete_project_role(project, request):
             )
 
             request.session.flash("Removed role", queue="success")
+            if removing_self:
+                return HTTPSeeOther(request.route_path("manage.projects"))
     except NoResultFound:
         request.session.flash("Could not find role", queue="error")
 


Test patch:
diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py
--- a/tests/unit/manage/test_views.py
+++ b/tests/unit/manage/test_views.py
@@ -4031,7 +4031,9 @@ def test_add_organization_project_new_project_name_conflict(
 
 
 class TestManageOrganizationRoles:
-    def test_get_manage_organization_roles(self, db_request, enable_organizations):
+    def test_get_manage_organization_roles(
+        self, db_request, pyramid_user, enable_organizations
+    ):
         organization = OrganizationFactory.create(name="foobar")
         form_obj = pretend.stub()
 
@@ -4046,6 +4048,7 @@ def form_class(*a, **kw):
             "roles": set(),
             "invitations": set(),
             "form": form_obj,
+            "organizations_with_sole_owner": [],
         }
 
     @freeze_time(datetime.datetime.utcnow())
@@ -4810,10 +4813,20 @@ def test_delete_role(self, db_request, enable_organizations, monkeypatch):
         assert isinstance(result, HTTPSeeOther)
         assert result.headers["Location"] == "/the-redirect"
 
-    def test_delete_missing_role(self, db_request, enable_organizations):
+    def test_delete_missing_role(self, db_request, enable_organizations, monkeypatch):
         organization = OrganizationFactory.create(name="foobar")
         missing_role_id = str(uuid.uuid4())
 
+        user_organizations = pretend.call_recorder(
+            lambda *a, **kw: {
+                "organizations_managed": [],
+                "organizations_owned": [organization],
+                "organizations_billing": [],
+                "organizations_with_sole_owner": [],
+            }
+        )
+        monkeypatch.setattr(views, "user_organizations", user_organizations)
+
         db_request.method = "POST"
         db_request.user = pretend.stub()
         db_request.POST = MultiDict({"role_id": missing_role_id})
@@ -4880,7 +4893,7 @@ def test_delete_own_owner_role(self, db_request, enable_organizations):
         result = views.delete_organization_role(organization, db_request)
 
         assert db_request.session.flash.calls == [
-            pretend.call("Cannot remove yourself as Owner", queue="error")
+            pretend.call("Cannot remove yourself as Sole Owner", queue="error")
         ]
         assert isinstance(result, HTTPSeeOther)
         assert result.headers["Location"] == "/the-redirect"
@@ -8641,11 +8654,66 @@ def test_delete_own_owner_role(self, db_request):
         result = views.delete_project_role(project, db_request)
 
         assert db_request.session.flash.calls == [
-            pretend.call("Cannot remove yourself as Owner", queue="error")
+            pretend.call("Cannot remove yourself as Sole Owner", queue="error")
         ]
         assert isinstance(result, HTTPSeeOther)
         assert result.headers["Location"] == "/the-redirect"
 
+    def test_delete_not_sole_owner_role(self, db_request, monkeypatch):
+        project = ProjectFactory.create(name="foobar")
+        user = UserFactory.create()
+        RoleFactory.create(user=user, project=project, role_name="Owner")
+        user_2 = UserFactory.create(username="testuser")
+        role_2 = RoleFactory.create(user=user_2, project=project, role_name="Owner")
+
+        db_request.method = "POST"
+        db_request.user = user_2
+        db_request.POST = MultiDict({"role_id": role_2.id})
+        db_request.session = pretend.stub(
+            flash=pretend.call_recorder(lambda *a, **kw: None)
+        )
+        db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+
+        send_collaborator_removed_email = pretend.call_recorder(lambda *a, **kw: None)
+        monkeypatch.setattr(
+            views, "send_collaborator_removed_email", send_collaborator_removed_email
+        )
+        send_removed_as_collaborator_email = pretend.call_recorder(
+            lambda *a, **kw: None
+        )
+        monkeypatch.setattr(
+            views,
+            "send_removed_as_collaborator_email",
+            send_removed_as_collaborator_email,
+        )
+
+        result = views.delete_project_role(project, db_request)
+
+        assert db_request.route_path.calls == [pretend.call("manage.projects")]
+        assert db_request.db.query(Role).filter(Role.user_id == user_2.id).all() == []
+        assert send_collaborator_removed_email.calls == [
+            pretend.call(
+                db_request, {user}, user=user_2, submitter=user_2, project_name="foobar"
+            )
+        ]
+        assert send_removed_as_collaborator_email.calls == [
+            pretend.call(db_request, user_2, submitter=user_2, project_name="foobar")
+        ]
+        assert db_request.session.flash.calls == [
+            pretend.call("Removed role", queue="success")
+        ]
+        assert isinstance(result, HTTPSeeOther)
+        assert result.headers["Location"] == "/the-redirect"
+
+        entry = (
+            db_request.db.query(JournalEntry).options(joinedload("submitted_by")).one()
+        )
+
+        assert entry.name == project.name
+        assert entry.action == "remove Owner testuser"
+        assert entry.submitted_by == db_request.user
+        assert entry.submitted_from == db_request.remote_addr
+
     def test_delete_non_owner_role(self, db_request):
         project = ProjectFactory.create(name="foobar")
         user = UserFactory.create(username="testuser")