CVE-2022-24841: Incorrect Authorization (CWE-863)
value: fleetdm/fleet is an open source device management, built on osquery. All versions of fleet making use of the teams feature are affected by this authorization bypass issue. Fleet instances without teams, or with teams but without restricted team accounts are not affected. In affected versions a team admin can erroneously add themselves as admin, maintainer or observer on other teams. Users are advised to upgrade to version 4.13. There are no known workarounds for this issue.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 8.1
Published: 2022-04-18T22:15Z
Repository: https://github.com/fleetdm/fleet
Language: Go
Files changed: changes/activities-rbac, changes/issue-GHSA-pr2g-j78h-84cr, cypress/integration/free/maintainer.spec.ts, cypress/integration/free/observer.spec.ts, cypress/integration/premium/admin.spec.ts, cypress/integration/premium/maintainer.spec.ts, cypress/integration/premium/team_admin.spec.ts, cypress/integration/premium/team_maintainer_observer.spec.ts, frontend/pages/Homepage/cards/Software/Software.tsx, frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx, frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx, frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx, frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx, server/authz/authz.go, server/authz/policy.rego, server/authz/policy_test.go, server/fleet/authz.go, server/fleet/packs.go, server/fleet/software.go, server/service/activities_test.go, server/service/endpoint_utils_test.go, server/service/global_schedule.go, server/service/global_schedule_test.go, server/service/hosts.go, server/service/scheduled_queries_test.go, server/service/software.go, server/service/software_test.go, server/service/team_schedule.go, server/service/team_schedule_test.go, server/service/users.go, server/service/users_test.go
Commit date: 2022-04-18 10:27:30 -0700
Commit message: Merge pull request from GHSA-pr2g-j78h-84cr

* Fix access control issues with users

* Fix access control issues with packs

* Fix access control issues with software

* Changes suggested by Martin

* All users can access the global schedule

* Restrict access to activities

* Add explicit test for team admin escalation vuln

* All global users should be able to read all software

* Handbook editor pass - Security - GitHub Security (#5108)

* Update security.md

All edits are recorded by line:

395 replaced “open-source” with “open source”
411 replaced “open-source” with “open source”
439 added “the” before “comment”; replaced “repositories,” with “repositories”
445 deleted “being” before “located”
458 added “and” after “PR”
489 replaced “on” with “in”
493 replaced “open-source” with “open source”; Replaced “privileges,” with “privileges”

* Update security.md

line 479

* Update security.md

added (static analysis tools used to identify problems in code) to line 479

* Fix UI

* Fix UI

* revert api v1 to latest in documentation (#5149)

* revert api v1 to latest in documentation

* Update fleetctl doc page

Co-authored-by: Noah Talerman <noahtal@umich.edu>

* Add team admin team policy automation; fix e2e

* Update to company page of the handbook (#5164)

Updated "Why do we use a wireframe-first approach?" section of company.md

* removed extra data on smaller screens (#5154)

* Update for team automations; e2e

* Jira Integration: Cypress e2e tests only (#5055)

* Update company.md (#5170)

This is to update the formatting under "empathy" and to fix the spelling of "help text."
This was done as per @mikermcneil .
This is related to #https://github.com/fleetdm/fleet/pull/4941 and https://github.com/fleetdm/fleet/issues/4902

* fix update updated_at for aggregated_stats (#5112)

Update the updated_at column when using ON DUPLICATE UPDATE so that
the counts_updated_at is up to date

* basic sql formatting in code ie whitespace around operators

* Fix e2e test

* Fix tests in server/authz

Co-authored-by: gillespi314 <73313222+gillespi314@users.noreply.github.com>
Co-authored-by: Desmi-Dizney <99777687+Desmi-Dizney@users.noreply.github.com>
Co-authored-by: Michal Nicpon <39177923+michalnicp@users.noreply.github.com>
Co-authored-by: Noah Talerman <noahtal@umich.edu>
Co-authored-by: Mike Thomas <78363703+mike-j-thomas@users.noreply.github.com>
Co-authored-by: Martavis Parker <47053705+martavis@users.noreply.github.com>
Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
Diff: diff --git a/changes/activities-rbac b/changes/activities-rbac
new file mode 100644
index 000000000..d8b80a4ef
--- /dev/null
+++ b/changes/activities-rbac
@@ -0,0 +1 @@
+* Restrict non-global user from access to activities
\ No newline at end of file
diff --git a/changes/issue-GHSA-pr2g-j78h-84cr b/changes/issue-GHSA-pr2g-j78h-84cr
new file mode 100644
index 000000000..e66b072a4
--- /dev/null
+++ b/changes/issue-GHSA-pr2g-j78h-84cr
@@ -0,0 +1,3 @@
+* Fix access control issues with "user" endpoints.
+* Fix access control issues with "pack" endpoints.
+* Fix access control issues with "software" endpoints.
diff --git a/cypress/integration/free/maintainer.spec.ts b/cypress/integration/free/maintainer.spec.ts
index 994d24d5f..a5419ef5e 100644
--- a/cypress/integration/free/maintainer.spec.ts
+++ b/cypress/integration/free/maintainer.spec.ts
@@ -187,35 +187,11 @@ describe(
       });
     });
     describe("Manage software page", () => {
       beforeEach(() => cy.visit("/software/manage"));
       it("should restrict global maintainer from 'Manage automations' button", () => {
-        it("hides manages software automations when all teams selected", () => {
-          cy.getAttached(".manage-software-page__header-wrap").within(() => {
-            cy.getAttached(".Select").within(() => {
-              cy.findByText(/all teams/i).should("exist");
-            });
-            cy.getAttached(".manage-software-page__header-wrap").within(() => {
-              cy.findByRole("button", {
-                name: /manage automations/i,
-              }).should("not.exist");
-            });
-          });
-        });
-        it("hides manage automations button when all teams not selected", () => {
-          cy.getAttached(".manage-software-page__header-wrap").within(() => {
-            cy.getAttached(".Select").within(() => {
-              cy.getAttached(".Select-control").click();
-              cy.getAttached(".Select-menu-outer").within(() => {
-                cy.findByText(/apples/i).should("exist");
-              });
-              cy.findByRole("button", {
-                name: /manage automations/i,
-              }).should("not.exist");
-            });
-          });
-        });
+        cy.findByText(/manage automations/).should("not.exist");
       });
     });
     describe("Query pages", () => {
       beforeEach(() => {
         cy.loginWithCySession("mary@organization.com", "user123#");
@@ -272,13 +248,17 @@ describe(
     describe("Manage policies page", () => {
       beforeEach(() => {
         cy.loginWithCySession("mary@organization.com", "user123#");
         cy.visit("/policies/manage");
       });
-      it("allows maintainer to manage automations", () => {
-        cy.findByRole("button", { name: /manage automations/i }).click();
-        cy.findByRole("button", { name: /cancel/i }).click();
+      it("hides manage automations from maintainer", () => {
+        cy.getAttached(".button-wrap").within(() => {
+          cy.findByRole("button", { name: /add a policy/i }).should("exist");
+          cy.findByRole("button", { name: /manage automations/i }).should(
+            "not.exist"
+          );
+        });
       });
       it("allows maintainer to add a policy", () => {
         cy.findByRole("button", { name: /add a policy/i }).click();
         cy.getAttached(".modal__ex").within(() => {
           cy.findByRole("button").click();
diff --git a/cypress/integration/free/observer.spec.ts b/cypress/integration/free/observer.spec.ts
index 689f3a57b..bff92244a 100644
--- a/cypress/integration/free/observer.spec.ts
+++ b/cypress/integration/free/observer.spec.ts
@@ -207,16 +207,14 @@ describe("Free tier - Observer user", () => {
     beforeEach(() => {
       cy.loginWithCySession("oliver@organization.com", "user123#");
       cy.visit("/policies/manage");
     });
     it("hides manage automations button", () => {
-      cy.findByRole("button", { name: /manage automations/i }).should(
-        "not.exist"
-      );
+      cy.findByText(/manage automations/i).should("not.exist");
     });
     it("hides add a policy button", () => {
-      cy.findByRole("button", { name: /add a policy/i }).should("not.exist");
+      cy.findByText(/add a policy/).should("not.exist");
     });
     it("hides run, edit, or delete a policy", () => {
       cy.getAttached("tbody").within(() => {
         cy.getAttached("tr")
           .first()
diff --git a/cypress/integration/premium/admin.spec.ts b/cypress/integration/premium/admin.spec.ts
index d6c508223..974342d4b 100644
--- a/cypress/integration/premium/admin.spec.ts
+++ b/cypress/integration/premium/admin.spec.ts
@@ -562,10 +562,31 @@ describe("Premium tier - Global Admin user", () => {
           .type("https://example.com/global_admin");
         cy.findByText(/save/i).click();
       });
       cy.findByText(/successfully updated policy automations/i).should("exist");
     });
+    it("allows global admin to automate a team policy", () => {
+      cy.visit("/policies/manage");
+      cy.getAttached(".Select-control").within(() => {
+        cy.findByText(/all teams/i).click();
+      });
+      cy.getAttached(".Select-menu")
+        .contains(/apples/i)
+        .click();
+      cy.getAttached(".button-wrap")
+        .findByRole("button", { name: /manage automations/i })
+        .click();
+      cy.getAttached(".manage-automations-modal").within(() => {
+        cy.getAttached(".fleet-slider").click();
+        cy.getAttached(".fleet-checkbox__input").check({ force: true });
+        cy.getAttached("#webhook-url")
+          .clear()
+          .type("https://example.com/global_admin");
+        cy.findByText(/save/i).click();
+      });
+      cy.findByText(/successfully updated policy automations/i).should("exist");
+    });
     it("allows global admin to delete a team policy", () => {
       cy.visit("/policies/manage");
       cy.getAttached(".Select-control").within(() => {
         cy.findByText(/all teams/i).click();
       });
diff --git a/cypress/integration/premium/maintainer.spec.ts b/cypress/integration/premium/maintainer.spec.ts
index 4c88e81fe..0e83b1188 100644
--- a/cypress/integration/premium/maintainer.spec.ts
+++ b/cypress/integration/premium/maintainer.spec.ts
@@ -199,36 +199,12 @@ describe("Premium tier - Maintainer user", () => {
         });
       });
     });
     describe("Manage software page", () => {
       beforeEach(() => cy.visit("/software/manage"));
-      it("should restrict global maintainer from 'Manage automations' button", () => {
-        it("hides manages software automations when all teams selected", () => {
-          cy.getAttached(".manage-software-page__header-wrap").within(() => {
-            cy.getAttached(".Select").within(() => {
-              cy.findByText(/all teams/i).should("exist");
-            });
-            cy.getAttached(".manage-software-page__header-wrap").within(() => {
-              cy.findByRole("button", {
-                name: /manage automations/i,
-              }).should("not.exist");
-            });
-          });
-        });
-        it("hides manage automations button when all teams not selected", () => {
-          cy.getAttached(".manage-software-page__header-wrap").within(() => {
-            cy.getAttached(".Select").within(() => {
-              cy.getAttached(".Select-control").click();
-              cy.getAttached(".Select-menu-outer").within(() => {
-                cy.findByText(/apples/i).should("exist");
-              });
-              cy.findByRole("button", {
-                name: /manage automations/i,
-              }).should("not.exist");
-            });
-          });
-        });
+      it("hides 'Manage automations' button from global maintainer", () => {
+        cy.findByText(/manage automations/i).should("not.exist");
       });
     });
     describe("Query pages", () => {
       beforeEach(() => cy.visit("/queries/manage"));
       it("allows global maintainer to select teams targets for query", () => {
@@ -248,15 +224,12 @@ describe("Premium tier - Maintainer user", () => {
         cy.contains(".selector-name", /apples/i).should("exist");
       });
     });
     describe("Manage policies page", () => {
       beforeEach(() => cy.visit("/policies/manage"));
-      it("allows global maintainer to click 'Manage automations' button", () => {
-        cy.getAttached(".button-wrap")
-          .findByRole("button", { name: /manage automations/i })
-          .click();
-        cy.findByRole("button", { name: /cancel/i }).click();
+      it("hides manage automations button", () => {
+        cy.findByText(/manage hosts/i).should("not.exist");
       });
       it("allows global maintainer to add a new policy", () => {
         cy.getAttached(".button-wrap")
           .findByRole("button", { name: /add a polic/i })
           .click();
diff --git a/cypress/integration/premium/team_admin.spec.ts b/cypress/integration/premium/team_admin.spec.ts
index 3875acd03..93d270190 100644
--- a/cypress/integration/premium/team_admin.spec.ts
+++ b/cypress/integration/premium/team_admin.spec.ts
@@ -190,14 +190,11 @@ describe("Premium tier - Team Admin user", () => {
       });
     });
   });
   describe("Manage software page", () => {
     beforeEach(() => cy.visit("/software/manage"));
-    it("hides manage automations button since all teams not selected", () => {
-      cy.getAttached(".manage-software-page__header-wrap").within(() => {
-        cy.findByText(/apples/i).should("exist");
-      });
+    it("hides manage automations button", () => {
       cy.findByText(/manage automations/i).should("not.exist");
     });
   });
   describe("Query pages", () => {
     beforeEach(() => cy.visit("/queries/manage"));
@@ -281,16 +278,10 @@ describe("Premium tier - Team Admin user", () => {
       cy.findByText(/successfully removed/i).should("be.visible");
     });
   });
   describe("Manage policies page", () => {
     beforeEach(() => cy.visit("/policies/manage"));
-    it("hides manage automations button when all teams not selected", () => {
-      cy.getAttached(".manage-policies-page__header-wrap").within(() => {
-        cy.findByText(/apples/i).should("exist");
-      });
-      cy.findByText(/manage automations/i).should("not.exist");
-    });
     it("allows team admin to add a new policy", () => {
       cy.getAttached(".button-wrap")
         .findByRole("button", { name: /add a policy/i })
         .click();
       // Add a default policy
@@ -356,11 +347,11 @@ describe("Premium tier - Team Admin user", () => {
     it("allows team admin to access team settings", () => {
       // Access the Settings - Team details page
       cy.findByText(/apples/i).should("exist");
     });
     it("displays the team admin controls", () => {
-      cy.findByRole("button", { name: /add member/i }).click();
+      cy.findByRole("button", { name: /create user/i }).click();
       cy.findByRole("button", { name: /cancel/i }).click();
       cy.findByRole("button", { name: /add hosts/i }).click();
       cy.findByRole("button", { name: /done/i }).click();
       cy.findByRole("button", { name: /manage enroll secrets/i }).click();
       cy.findByRole("button", { name: /done/i }).click();
diff --git a/cypress/integration/premium/team_maintainer_observer.spec.ts b/cypress/integration/premium/team_maintainer_observer.spec.ts
index 2fe9585d1..a861b4d4e 100644
--- a/cypress/integration/premium/team_maintainer_observer.spec.ts
+++ b/cypress/integration/premium/team_maintainer_observer.spec.ts
@@ -274,12 +274,12 @@ describe("Premium tier - Team observer/maintainer user", () => {
         cy.visit("/policies/manage");
         // Switch to from team apples to team oranges
         cy.findByText(/apples/i).click();
         cy.findByText(/oranges/i).click();
 
-        // On maintaining team, should see the "Manage automations" button
-        cy.findByText(/manage automations/i).should("exist");
+        // On maintaining team, should not see the "Manage automations" button
+        cy.findByText(/manage automations/i).should("not.exist");
         // On maintaining team, should see "add a policy" and "save" a policy
         cy.findByText(/add a policy/i).click();
 
         // Add a default policy
         cy.findByText(/gatekeeper enabled/i).click();
diff --git a/frontend/pages/Homepage/cards/Software/Software.tsx b/frontend/pages/Homepage/cards/Software/Software.tsx
index 7a07e00d3..6615d6451 100644
--- a/frontend/pages/Homepage/cards/Software/Software.tsx
+++ b/frontend/pages/Homepage/cards/Software/Software.tsx
@@ -1,9 +1,10 @@
-import React, { useState } from "react";
+import React, { useContext, useState } from "react";
 import { useQuery } from "react-query";
 import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
 
+import { AppContext } from "context/app";
 import paths from "router/paths";
 import configAPI from "services/entities/config";
 import softwareAPI, { ISoftwareResponse } from "services/entities/software";
 
 import TabsWrapper from "components/TabsWrapper";
@@ -37,10 +38,14 @@ const Software = ({
 }: ISoftwareCardProps): JSX.Element => {
   const [navTabIndex, setNavTabIndex] = useState<number>(0);
   const [pageIndex, setPageIndex] = useState<number>(0);
   const [isSoftwareEnabled, setIsSoftwareEnabled] = useState<boolean>();
 
+  const { availableTeams, currentTeam, isOnGlobalTeam } = useContext(
+    AppContext
+  );
+
   const { data: config } = useQuery(["config"], configAPI.loadAll, {
     onSuccess: (data) => {
       setIsSoftwareEnabled(data?.host_settings?.enable_software_inventory);
     },
   });
@@ -69,12 +74,15 @@ const Software = ({
         orderDir: DEFAULT_SORT_DIRECTION,
         vulnerable: !!navTabIndex, // we can take the tab index as a boolean to represent the vulnerable flag :)
         teamId: currentTeamId,
       }),
     {
+      enabled:
+        isOnGlobalTeam ||
+        !!availableTeams?.find((t) => t.id === currentTeam?.id),
       keepPreviousData: true,
-      staleTime: 30000, // TODO: Discuss a reasonable staleTime given that counts are only updated infrequently?
+      staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval
       onSuccess: (data) => {
         setShowSoftwareUI(true);
         if (isSoftwareEnabled && data.software?.length !== 0) {
           setTitleDetail &&
             setTitleDetail(
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx
index 8b65ae1b4..ba854b48d 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx
@@ -49,13 +49,17 @@ const MembersPage = ({
   params: { team_id },
 }: IMembersPageProps): JSX.Element => {
   const teamId = parseInt(team_id, 10);
 
   const { renderFlash } = useContext(NotificationContext);
-  const { config, isGlobalAdmin, currentUser, isPremiumTier } = useContext(
-    AppContext
-  );
+  const {
+    config,
+    currentUser,
+    isGlobalAdmin,
+    isPremiumTier,
+    isTeamAdmin,
+  } = useContext(AppContext);
 
   const smtpConfigured = config?.smtp_settings.configured || false;
   const canUseSso = config?.sso_settings.enable_sso || false;
 
   const [showAddMemberModal, setShowAddMemberModal] = useState<boolean>(false);
@@ -135,27 +139,23 @@ const MembersPage = ({
   );
 
   const toggleCreateMemberModal = useCallback(() => {
     setShowCreateUserModal(!showCreateUserModal);
     setShowAddMemberModal(false);
-    currentUser ? setUserEditing(currentUser) : setUserEditing(undefined);
-  }, [
-    showCreateUserModal,
-    currentUser,
-    setShowCreateUserModal,
-    setUserEditing,
-    setShowAddMemberModal,
-  ]);
+  }, [showCreateUserModal, setShowCreateUserModal, setShowAddMemberModal]);
 
   // FUNCTIONS
 
   const onRemoveMemberSubmit = useCallback(() => {
     const removedUsers = { users: [{ id: userEditing?.id }] };
     teamsAPI
       .removeMembers(teamId, removedUsers)
       .then(() => {
-        renderFlash("success", `Successfully removed ${userEditing?.name}`);
+        renderFlash(
+          "success",
+          `Successfully removed ${userEditing?.name || "member"}`
+        );
         // If user removes self from team, redirect to home
         if (currentUser && currentUser.id === removedUsers.users[0].id) {
           window.location.href = "/";
         }
       })
@@ -176,16 +176,19 @@ const MembersPage = ({
 
   const onAddMemberSubmit = useCallback(
     (newMembers: INewMembersBody) => {
       teamsAPI
         .addMembers(teamId, newMembers)
-        .then(() =>
+        .then(() => {
+          const count = newMembers.users.length;
           renderFlash(
             "success",
-            `${newMembers.users.length} members successfully added to ${currentTeam?.name}.`
-          )
-        )
+            `${count} ${
+              count === 1 ? "member" : "members"
+            } successfully added to ${currentTeam?.name}.`
+          );
+        })
         .catch(() =>
           renderFlash("error", "Could not add members. Please try again.")
         )
         .finally(() => {
           toggleAddUserModal();
@@ -286,11 +289,14 @@ const MembersPage = ({
 
       userEditing &&
         usersAPI
           .update(userEditing.id, updatedAttrs)
           .then(() => {
-            renderFlash("success", `Successfully edited ${userName}.`);
+            renderFlash(
+              "success",
+              `Successfully edited ${userName || "member"}.`
+            );
 
             if (
               currentUser &&
               userEditing &&
               currentUser.id === userEditing.id
@@ -315,11 +321,11 @@ const MembersPage = ({
                 email: "A user with this email address already exists",
               });
             } else {
               renderFlash(
                 "error",
-                `Could not edit ${userName}. Please try again.`
+                `Could not edit ${userName || "member"}. Please try again.`
               );
             }
           });
     },
     [toggleEditMemberModal, userEditing, refetchUsers]
@@ -347,17 +353,28 @@ const MembersPage = ({
                 <h1>This team doesn&apos;t have any members yet.</h1>
                 <p>
                   Expecting to see new team members listed here? Try again in a
                   few seconds as the system catches up.
                 </p>
-                <Button
-                  variant="brand"
-                  className={`${noMembersClass}__create-button`}
-                  onClick={toggleAddUserModal}
-                >
-                  Add member
-                </Button>
+                {isGlobalAdmin && (
+                  <Button
+                    variant="brand"
+                    className={`${noMembersClass}__create-button`}
+                    onClick={toggleAddUserModal}
+                  >
+                    Add member
+                  </Button>
+                )}
+                {isTeamAdmin && (
+                  <Button
+                    variant="brand"
+                    className={`${noMembersClass}__create-button`}
+                    onClick={toggleCreateMemberModal}
+                  >
+                    Create user
+                  </Button>
+                )}
               </>
             ) : (
               <>
                 <h2>We couldn’t find any members.</h2>
                 <p>
@@ -394,12 +411,14 @@ const MembersPage = ({
           columns={tableHeaders}
           data={members || []}
           isLoading={isLoadingMembers}
           defaultSortHeader={"name"}
           defaultSortDirection={"asc"}
-          onActionButtonClick={toggleAddUserModal}
-          actionButtonText={"Add member"}
+          onActionButtonClick={
+            isGlobalAdmin ? toggleAddUserModal : toggleCreateMemberModal
+          }
+          actionButtonText={isGlobalAdmin ? "Add member" : "Create user"}
           actionButtonVariant={"brand"}
           hideActionButton={memberIds.length === 0 && searchString === ""}
           onQueryChange={({ searchQuery }) => setSearchString(searchQuery)}
           inputPlaceHolder={"Search"}
           emptyComponent={NoMembersComponent}
@@ -422,11 +441,11 @@ const MembersPage = ({
           editUserErrors={editUserErrors}
           onCancel={toggleEditMemberModal}
           onSubmit={onEditMemberSubmit}
           defaultName={userEditing?.name}
           defaultEmail={userEditing?.email}
-          defaultGlobalRole={userEditing?.global_role}
+          defaultGlobalRole={userEditing?.global_role || null}
           defaultTeamRole={userEditing?.role}
           defaultTeams={userEditing?.teams}
           availableTeams={teams || []}
           isPremiumTier={isPremiumTier || false}
           smtpConfigured={smtpConfigured}
diff --git a/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx b/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx
index 033f1cf9e..50a25faba 100644
--- a/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx
+++ b/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx
@@ -119,11 +119,11 @@ const UserForm = ({
     teams: defaultTeams || [],
     currentUserId,
   });
 
   const [isGlobalUser, setIsGlobalUser] = useState<boolean>(
-    defaultGlobalRole !== null
+    !!defaultGlobalRole
   );
 
   useEffect(() => {
     setErrors(createOrEditUserErrors);
   }, [createOrEditUserErrors]);
diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
index c956be1ce..6d508bc98 100644
--- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
@@ -136,10 +136,11 @@ const ManagePolicyPage = ({
     }
   );
 
   const canAddOrRemovePolicy =
     isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer || isTeamAdmin;
+  const canManageAutomations = isGlobalAdmin || isTeamAdmin;
 
   const { isLoading: isLoadingWebhooks, refetch: refetchWebhooks } = useQuery<
     IConfig | ILoadTeamResponse,
     Error,
     IConfig | ITeam
@@ -362,11 +363,11 @@ const ManagePolicyPage = ({
                   )}
               </div>
             </div>
           </div>
           <div className={`${baseClass} button-wrap`}>
-            {canAddOrRemovePolicy &&
+            {canManageAutomations &&
               !isLoadingWebhooks &&
               !isLoadingGlobalPolicies && (
                 <Button
                   onClick={toggleManageAutomationsModal}
                   className={`${baseClass}__manage-automations button`}
diff --git a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx
index 6f9ca10c7..3c1e3a60e 100644
--- a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx
+++ b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx
@@ -66,13 +66,13 @@ const ManageSoftwarePage = ({
   location,
 }: IManageSoftwarePageProps): JSX.Element => {
   const {
     availableTeams,
     currentTeam,
-    setConfig,
     isGlobalAdmin,
     isGlobalMaintainer,
+    isOnGlobalTeam,
   } = useContext(AppContext);
   const { renderFlash } = useContext(NotificationContext);
 
   const [isSoftwareEnabled, setIsSoftwareEnabled] = useState<boolean>();
   const [
@@ -160,10 +160,13 @@ const ManageSoftwarePage = ({
         teamId: currentTeam?.id,
       };
       return softwareAPI.load(params);
     },
     {
+      enabled:
+        isOnGlobalTeam ||
+        !!availableTeams?.find((t) => t.id === currentTeam?.id),
       keepPreviousData: true,
       staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval
     }
   );
 
@@ -188,10 +191,13 @@ const ManageSoftwarePage = ({
         vulnerable: !!location.query.vulnerable,
         teamId: currentTeam?.id,
       });
     },
     {
+      enabled:
+        isOnGlobalTeam ||
+        !!availableTeams?.find((t) => t.id === currentTeam?.id),
       keepPreviousData: true,
       staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval
       refetchOnWindowFocus: false,
       retry: 1,
       select: (data) => data.count,
diff --git a/server/authz/authz.go b/server/authz/authz.go
index 98cb7909e..81f627ff9 100644
--- a/server/authz/authz.go
+++ b/server/authz/authz.go
@@ -136,10 +136,16 @@ func (a *Authorizer) Authorize(ctx context.Context, object, action interface{})
 type AuthzTyper interface {
 	// AuthzType returns the type as a snake_case string.
 	AuthzType() string
 }
 
+// ExtraAuthzer is the interface to implement extra fields for the policy.
+type ExtraAuthzer interface {
+	// ExtraAuthz returns the extra key/value pairs for the type.
+	ExtraAuthz() (map[string]interface{}, error)
+}
+
 // jsonToInterface turns any type that can be JSON (un)marshaled into an
 // map[string]interface{} for evaluation by the OPA engine. Nil is returned as nil.
 func jsonToInterface(in interface{}) (interface{}, error) {
 	// Special cases for nil and string.
 	if in == nil {
@@ -167,10 +173,23 @@ func jsonToInterface(in interface{}) (interface{}, error) {
 
 	// Add the `type` property if the AuthzTyper interface is implemented.
 	if typer, ok := in.(AuthzTyper); ok {
 		out["type"] = typer.AuthzType()
 	}
+	// Add any extra key/values defined by the type.
+	if extra, ok := in.(ExtraAuthzer); ok {
+		extraKVs, err := extra.ExtraAuthz()
+		if err != nil {
+			return nil, fmt.Errorf("extra authz: %w", err)
+		}
+		for k, v := range extraKVs {
+			if _, ok := out[k]; ok {
+				return nil, fmt.Errorf("existing authz value: %s", k)
+			}
+			out[k] = v
+		}
+	}
 
 	return out, nil
 }
 
 // UserFromContext retrieves a user from the viewer context, returning nil if
diff --git a/server/authz/policy.rego b/server/authz/policy.rego
index 8d2244a56..889319bdd 100644
--- a/server/authz/policy.rego
+++ b/server/authz/policy.rego
@@ -11,12 +11,16 @@ import input.subject
 
 # Actions
 read := "read"
 list := "list"
 write := "write"
+
+# User specific actions
 write_role := "write_role"
 change_password := "change_password"
+
+# Query specific actions
 run := "run"
 run_new := "run_new"
 
 # Roles
 admin := "admin"
@@ -93,39 +97,35 @@ allow {
   action == write
 }
 
 ##
 # Users
+#
+# NOTE: More rules apply to users but they are implemented in Go code.
+# Our end goal is to move all the authorization logic here.
 ##
 
-# Any user can write self (besides role) and change their own password.
+# Any user can read and write self and change their own password.
 allow {
   object.type == "user"
   object.id == subject.id
   object.id != 0
-  action == [write, change_password][_]
+  action == [read, write, change_password][_]
 }
 
-# Any user can read other users.
-allow {
-  object.type == "user"
-  not is_null(subject)
-  action == read
-}
-
-# Admins can write all users + roles + change passwords.
+# Global admins can perform all operations on all users.
 allow {
   object.type == "user"
   subject.global_role == admin
-  action == [write, write_role, change_password][_]
+  action == [read, write, write_role, change_password][_]
 }
 
-## Team admins can create or edit new users, but not change their password.
+# Team admins can perform all operations on the team users (except changing their password).
 allow {
   object.type == "user"
   team_role(subject, object.teams[_].id) == admin
-  action == [write, write_role][_]
+  action == [read, write, write_role][_]
 }
 
 ##
 # Invites
 ##
@@ -139,13 +139,13 @@ allow {
 
 ##
 # Activities
 ##
 
-# All users can read activities
+# Only global users can read activities
 allow {
-  not is_null(subject)
+  not is_null(subject.global_role)
   object.type == "activity"
   action == read
 }
 
 ##
@@ -400,30 +400,45 @@ allow {
 
 ##
 # Packs
 ##
 
-# Global admins and maintainers can read/write all packs
+# Global admins and maintainers can read/write all packs.
 allow {
   object.type == "pack"
-  subject.global_role == [admin,maintainer][_]
+  subject.global_role == [admin, maintainer][_]
   action == [read, write][_]
 }
 
-# Team admins and maintainers can read global packs
+# All users can read the global pack.
 allow {
-  is_null(object.team_ids)
   object.type == "pack"
-  team_role(subject, subject.teams[_].id) == [admin,maintainer][_]
+  not is_null(subject)
+  object.is_global_pack == true
   action == read
 }
 
-# Team admins and maintainers can read/write their team packs
+# Team admins, maintainers and observers can read their team's pack.
+#
+# NOTE: Action "read" on a team's pack includes listing its scheduled queries.
 allow {
   object.type == "pack"
-  team_role(subject, object.team_ids[_]) == [admin,maintainer][_]
-  action == [read, write][_]
+  not is_null(object.pack_team_id)
+  team_role(subject, object.pack_team_id) == [admin, maintainer, observer][_]
+  action == read
+}
+
+# Team admins and maintainers can add/remove scheduled queries from/to their team's pack.
+#
+# NOTE: The team's pack is not editable per-se, it's a special pack to group
+# all the team's scheduled queries. So the "write" operation only covers
+# adding/removing scheduled queries from the pack.
+allow {
+  object.type == "pack"
+  not is_null(object.pack_team_id)
+  team_role(subject, object.pack_team_id) == [admin, maintainer][_]
+  action == write
 }
 
 ##
 # File Carves
 ##
@@ -479,11 +494,19 @@ allow {
 
 ##
 # Software
 ##
 
-# All users can read software
+# Global users can read all software.
 allow {
-  not is_null(subject)
-  object.type == "software"
+  object.type == "software_inventory"
+  subject.global_role == [admin, maintainer, observer][_]
   action == read
 }
+
+# Team users can read all software in their teams.
+allow {
+  not is_null(object.team_id)
+  object.type == "software_inventory"
+  team_role(subject, object.team_id) == [admin, maintainer, observer][_]
+  action == read
+}
\ No newline at end of file
diff --git a/server/authz/policy_test.go b/server/authz/policy_test.go
index e617aca7b..724528752 100644
--- a/server/authz/policy_test.go
+++ b/server/authz/policy_test.go
@@ -113,63 +113,75 @@ func TestAuthorizeUser(t *testing.T) {
 		{user: nil, object: user, action: write, allow: false},
 		{user: nil, object: user, action: writeRole, allow: false},
 		{user: nil, object: user, action: changePwd, allow: false},
 		{user: nil, object: newUser, action: write, allow: false},
 
-		// Admin can read/write all and create new
+		// Global admin can read/write all and create new.
 		{user: test.UserAdmin, object: user, action: read, allow: true},
 		{user: test.UserAdmin, object: user, action: write, allow: true},
 		{user: test.UserAdmin, object: user, action: writeRole, allow: true},
 		{user: test.UserAdmin, object: user, action: changePwd, allow: true},
 		{user: test.UserAdmin, object: newUser, action: write, allow: true},
 		{user: test.UserAdmin, object: test.UserAdmin, action: read, allow: true},
 		{user: test.UserAdmin, object: test.UserAdmin, action: write, allow: true},
 		{user: test.UserAdmin, object: test.UserAdmin, action: writeRole, allow: true},
 		{user: test.UserAdmin, object: test.UserAdmin, action: changePwd, allow: true},
 
-		// Regular users can read all users and write self (besides roles), but not create
-		{user: test.UserMaintainer, object: user, action: read, allow: true},
+		// Global maintainers cannot read/write users.
+		{user: test.UserMaintainer, object: user, action: read, allow: false},
 		{user: test.UserMaintainer, object: user, action: write, allow: false},
 		{user: test.UserMaintainer, object: user, action: writeRole, allow: false},
 		{user: test.UserMaintainer, object: user, action: changePwd, allow: false},
+		// Global maintainers cannot create users.
 		{user: test.UserMaintainer, object: newUser, action: write, allow: false},
+		// Global maintainers can read/write itself (besides roles).
 		{user: test.UserMaintainer, object: test.UserMaintainer, action: read, allow: true},
 		{user: test.UserMaintainer, object: test.UserMaintainer, action: write, allow: true},
 		{user: test.UserMaintainer, object: test.UserMaintainer, action: writeRole, allow: false},
 		{user: test.UserMaintainer, object: test.UserMaintainer, action: changePwd, allow: true},
 
-		{user: test.UserNoRoles, object: user, action: read, allow: true},
+		// Users without roles cannot read/write users.
+		{user: test.UserNoRoles, object: user, action: read, allow: false},
 		{user: test.UserNoRoles, object: user, action: write, allow: false},
 		{user: test.UserNoRoles, object: user, action: writeRole, allow: false},
 		{user: test.UserNoRoles, object: user, action: changePwd, allow: false},
+		// User without roles cannot add new users.
 		{user: test.UserNoRoles, object: newUser, action: write, allow: false},
+		// User without roles can read/write itself (besides roles).
 		{user: test.UserNoRoles, object: test.UserNoRoles, action: read, allow: true},
 		{user: test.UserNoRoles, object: test.UserNoRoles, action: write, allow: true},
 		{user: test.UserNoRoles, object: test.UserNoRoles, action: writeRole, allow: false},
 		{user: test.UserNoRoles, object: test.UserNoRoles, action: changePwd, allow: true},
 
-		{user: test.UserObserver, object: user, action: read, allow: true},
+		// Global observers cannot read/write users.
+		{user: test.UserObserver, object: user, action: read, allow: false},
 		{user: test.UserObserver, object: user, action: write, allow: false},
 		{user: test.UserObserver, object: user, action: writeRole, allow: false},
 		{user: test.UserObserver, object: user, action: changePwd, allow: false},
+		// Global observers cannot create users.
 		{user: test.UserObserver, object: newUser, action: write, allow: false},
+		// Global observers can read/write itself (besides roles).
 		{user: test.UserObserver, object: test.UserObserver, action: read, allow: true},
 		{user: test.UserObserver, object: test.UserObserver, action: write, allow: true},
 		{user: test.UserObserver, object: test.UserObserver, action: writeRole, allow: false},
 		{user: test.UserObserver, object: test.UserObserver, action: changePwd, allow: true},
 
-		// Team Admin can create/write/write role of any team user, but not change password
-		{user: teamAdmin, object: user, action: read, allow: true},
+		// Team admins cannot read/write global users.
+		{user: teamAdmin, object: user, action: read, allow: false},
 		{user: teamAdmin, object: user, action: write, allow: false},
 		{user: teamAdmin, object: user, action: writeRole, allow: false},
 		{user: teamAdmin, object: user, action: changePwd, allow: false},
+		// Team admins cannot create new global users.
 		{user: teamAdmin, object: newUser, action: write, allow: false},
+		// Team admins can read/write team users (except change their password).
 		{user: teamAdmin, object: teamObserver, action: read, allow: true},
 		{user: teamAdmin, object: teamObserver, action: write, allow: true},
 		{user: teamAdmin, object: teamObserver, action: writeRole, allow: true},
 		{user: teamAdmin, object: teamObserver, action: changePwd, allow: false},
+		// Team admins can add new users to the team.
 		{user: teamAdmin, object: newTeamUser, action: write, allow: true},
+		// Team admins can read/write itself.
 		{user: teamAdmin, object: teamAdmin, action: read, allow: true},
 		{user: teamAdmin, object: teamAdmin, action: write, allow: true},
 		{user: teamAdmin, object: teamAdmin, action: writeRole, allow: true},
 		{user: teamAdmin, object: teamAdmin, action: changePwd, allow: true},
 	})
@@ -620,64 +632,64 @@ func TestAuthorizeTeamPacks(t *testing.T) {
 	runTestCases(t, []authTestCase{
 		// Team maintainer can read packs of the team.
 		{
 			user: test.UserTeamMaintainerTeam1,
 			object: &fleet.Pack{
-				TeamIDs: []uint{1},
+				Type: ptr.String("team-1"),
 			},
 			action: read,
 			allow:  true,
 		},
-		// Team observer cannot read packs of the team.
+		// Team observer can read packs of the team.
 		{
 			user: test.UserTeamObserverTeam1TeamAdminTeam2,
 			object: &fleet.Pack{
-				TeamIDs: []uint{1},
+				Type: ptr.String("team-1"),
 			},
 			action: read,
-			allow:  false,
+			allow:  true,
 		},
 		// Team observer cannot write packs of the team.
 		{
 			user: test.UserTeamObserverTeam1TeamAdminTeam2,
 			object: &fleet.Pack{
-				TeamIDs: []uint{1},
+				Type: ptr.String("team-1"),
 			},
 			action: write,
 			allow:  false,
 		},
 		// Members of a team cannot read packs of another team.
 		{
 			user: test.UserTeamAdminTeam1,
 			object: &fleet.Pack{
-				TeamIDs: []uint{2},
+				Type: ptr.String("team-2"),
 			},
 			action: read,
 			allow:  false,
 		},
 		// Members of a team cannot read packs of another team.
 		{
 			user: test.UserTeamAdminTeam1,
 			object: &fleet.Pack{
-				TeamIDs: []uint{2},
+				Type: ptr.String("team-2"),
 			},
 			action: read,
 			allow:  false,
 		},
-		// Team maintainers can read global packs.
+		// Team maintainers cannot read global packs.
 		{
 			user:   test.UserTeamMaintainerTeam1,
 			object: &fleet.Pack{},
 			action: read,
-			allow:  true,
+			allow:  false,
 		},
-		// Team admins can read global packs.
+		// Team admins cannot read global packs.
 		{
 			user:   test.UserTeamAdminTeam1,
 			object: &fleet.Pack{},
 			action: read,
-			allow:  true,
+			allow:  false,
 		},
 		// Team admins cannot write global packs.
 		{
 			user:   test.UserTeamAdminTeam1,
 			object: &fleet.Pack{},
diff --git a/server/fleet/authz.go b/server/fleet/authz.go
index 01491e764..f73967ace 100644
--- a/server/fleet/authz.go
+++ b/server/fleet/authz.go
@@ -5,16 +5,30 @@ const (
 	ActionRead = "read"
 	// ActionList refers to listing an entity.
 	ActionList = "list"
 	// ActionWrite refers to writing (CRUD operations) an entity.
 	ActionWrite = "write"
+
+	//
+	// User specific actions
+	//
+
 	// ActionWriteRole is a write to a user's global roles and teams.
+	//
+	// While the Write action allows setting the role on creation of a user, the
+	// ActionWriteRole action is required to modify an existing user's password.
 	ActionWriteRole = "write_role"
-	// ActionChangePassword is the permission to change a user's password. While
-	// the Write action allows setting the password on creation of a user, the
-	// ChangePassword action is required to modify an existing user's password.
+	// ActionChangePassword is the permission to change a user's password.
+	//
+	// While the Write action allows setting the password on creation of a user, the
+	// ActionChangePassword action is required to modify an existing user's password.
 	ActionChangePassword = "change_password"
+
+	//
+	// Query specific actions
+	//
+
 	// ActionRun is the action for running a live query.
 	ActionRun = "run"
 	// ActionRunNew is the action for running a new live query.
 	ActionRunNew = "run_new"
 )
diff --git a/server/fleet/packs.go b/server/fleet/packs.go
index 014d53f87..286452e8b 100644
--- a/server/fleet/packs.go
+++ b/server/fleet/packs.go
@@ -1,33 +1,82 @@
 package fleet
 
 import (
 	"errors"
+	"strconv"
+	"strings"
+
+	"github.com/fleetdm/fleet/v4/server/ptr"
 )
 
 type PackListOptions struct {
 	ListOptions
 
 	// IncludeSystemPacks will include Global & Team Packs while listing packs
 	IncludeSystemPacks bool
 }
 
 // Pack is the structure which represents an osquery query pack.
+//
+// NOTE: A "team pack" is a special type of pack with Pack.Type="team-$TEAM_ID".
+// Such team packs hold the scheduled queries for a team. This is different from a
+// pack that has a team as target (Pack.Teams and Pack.TeamIDs fields).
 type Pack struct {
 	UpdateCreateTimestamps
-	ID          uint     `json:"id"`
-	Name        string   `json:"name"`
-	Description string   `json:"description,omitempty"`
-	Platform    string   `json:"platform,omitempty"`
-	Disabled    bool     `json:"disabled"`
-	Type        *string  `json:"type" db:"pack_type"`
-	Labels      []Target `json:"labels"`
-	LabelIDs    []uint   `json:"label_ids"`
-	Hosts       []Target `json:"hosts"`
-	HostIDs     []uint   `json:"host_ids"`
-	Teams       []Target `json:"teams"`
-	TeamIDs     []uint   `json:"team_ids"`
+	ID          uint   `json:"id"`
+	Name        string `json:"name"`
+	Description string `json:"description,omitempty"`
+	Platform    string `json:"platform,omitempty"`
+	Disabled    bool   `json:"disabled"`
+	// Type indicates the type of the pack:
+	//	- "global" is the type of the global pack.
+	//	- "team-$ID" is the type for team packs.
+	//	- nil is the type for a user created pack.
+	Type     *string  `json:"type" db:"pack_type"`
+	Labels   []Target `json:"labels"`
+	LabelIDs []uint   `json:"label_ids"`
+	Hosts    []Target `json:"hosts"`
+	HostIDs  []uint   `json:"host_ids"`
+	Teams    []Target `json:"teams"`
+	// TeamIDs holds the ID of the teams this pack should target.
+	TeamIDs []uint `json:"team_ids"`
+}
+
+// isTeamPack returns true if the pack is a pack specifically made for a team.
+func (p *Pack) isTeamPack() bool {
+	return p.Type != nil && strings.HasPrefix(*p.Type, "team-")
+}
+
+// isGlobalPack returns true if the pack is the global pack.
+func (p *Pack) isGlobalPack() bool {
+	return p.Type != nil && *p.Type == "global"
+}
+
+// TeamPack returns the team ID for a team's pack.
+// Returns (nil, nil) if the pack is not a team pack.
+func (p *Pack) teamPack() (*uint, error) {
+	if !p.isTeamPack() {
+		return nil, nil
+	}
+	t := strings.TrimPrefix(*p.Type, "team-")
+	teamID, err := strconv.ParseUint(t, 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	return ptr.Uint(uint(teamID)), nil
+}
+
+// ExtraAuthz implements authz.ExtraAuthzer.
+func (p *Pack) ExtraAuthz() (map[string]interface{}, error) {
+	packTeamID, err := p.teamPack()
+	if err != nil {
+		return nil, err
+	}
+	return map[string]interface{}{
+		"pack_team_id":   packTeamID,
+		"is_global_pack": p.isGlobalPack(),
+	}, nil
 }
 
 // Verify verifies the pack's fields are valid.
 func (p *Pack) Verify() error {
 	if emptyString(p.Name) {
diff --git a/server/fleet/software.go b/server/fleet/software.go
index 9479dfa13..b6e7fbd7c 100644
--- a/server/fleet/software.go
+++ b/server/fleet/software.go
@@ -41,10 +41,21 @@ type Software struct {
 
 func (Software) AuthzType() string {
 	return "software"
 }
 
+// AuthzSoftwareInventory is used for access controls on software inventory.
+type AuthzSoftwareInventory struct {
+	// TeamID is the ID of the team. A value of nil means global scope.
+	TeamID *uint `json:"team_id"`
+}
+
+// AuthzType implements authz.AuthzTyper.
+func (s *AuthzSoftwareInventory) AuthzType() string {
+	return "software_inventory"
+}
+
 type VulnerabilitiesSlice []SoftwareCVE
 
 // HostSoftware is the set of software installed on a specific host
 type HostSoftware struct {
 	// Software is the software information.
diff --git a/server/service/activities_test.go b/server/service/activities_test.go
index a32aef06d..4cd9f094c 100644
--- a/server/service/activities_test.go
+++ b/server/service/activities_test.go
@@ -13,26 +13,38 @@ import (
 
 func TestListActivities(t *testing.T) {
 	ds := new(mock.Store)
 	svc := newTestService(t, ds, nil, nil)
 
+	globalUsers := []*fleet.User{test.UserAdmin, test.UserMaintainer, test.UserObserver}
+	teamUsers := []*fleet.User{test.UserTeamAdminTeam1, test.UserTeamMaintainerTeam1, test.UserTeamObserverTeam1}
+
 	ds.ListActivitiesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Activity, error) {
 		return []*fleet.Activity{
 			{ID: 1},
 			{ID: 2},
 		}, nil
 	}
 
-	// admin user
-	activities, err := svc.ListActivities(test.UserContext(test.UserAdmin), fleet.ListOptions{})
-	require.NoError(t, err)
-	require.Len(t, activities, 2)
+	// any global user can read activities
+	for _, u := range globalUsers {
+		activities, err := svc.ListActivities(test.UserContext(u), fleet.ListOptions{})
+		require.NoError(t, err)
+		require.Len(t, activities, 2)
+	}
+
+	// team users cannot read activities
+	for _, u := range teamUsers {
+		_, err := svc.ListActivities(test.UserContext(u), fleet.ListOptions{})
+		require.Error(t, err)
+		require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
+	}
 
-	// anyone can read activities
-	activities, err = svc.ListActivities(test.UserContext(test.UserNoRoles), fleet.ListOptions{})
-	require.NoError(t, err)
-	require.Len(t, activities, 2)
+	// user with no roles cannot read activities
+	_, err := svc.ListActivities(test.UserContext(test.UserNoRoles), fleet.ListOptions{})
+	require.Error(t, err)
+	require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
 
 	// no user in context
 	_, err = svc.ListActivities(context.Background(), fleet.ListOptions{})
 	require.Error(t, err)
 	require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
diff --git a/server/service/endpoint_utils_test.go b/server/service/endpoint_utils_test.go
index 6ef852d30..7cabebbd3 100644
--- a/server/service/endpoint_utils_test.go
+++ b/server/service/endpoint_utils_test.go
@@ -9,11 +9,10 @@ import (
 	"net/url"
 	"strings"
 	"testing"
 	"time"
 
-	authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
 	"github.com/fleetdm/fleet/v4/server/fleet"
 	"github.com/fleetdm/fleet/v4/server/mock"
 	"github.com/fleetdm/fleet/v4/server/ptr"
 	"github.com/go-kit/kit/endpoint"
 	kitlog "github.com/go-kit/kit/log"
@@ -292,19 +291,15 @@ func TestEndpointer(t *testing.T) {
 		),
 	}
 
 	e := newUserAuthenticatedEndpointer(svc, fleetAPIOptions, r, "v1", "2021-11")
 	nopHandler := func(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
-		if authctx, ok := authz_ctx.FromContext(ctx); ok {
-			authctx.SetChecked()
-		}
+		setAuthCheckedOnPreAuthErr(ctx)
 		return "nop", nil
 	}
 	overrideHandler := func(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
-		if authctx, ok := authz_ctx.FromContext(ctx); ok {
-			authctx.SetChecked()
-		}
+		setAuthCheckedOnPreAuthErr(ctx)
 		return "override", nil
 	}
 
 	// Regular path, no plan to deprecate
 	e.GET("/api/_version_/fleet/path1", nopHandler, struct{}{})
diff --git a/server/service/global_schedule.go b/server/service/global_schedule.go
index afc6b77c2..8a65793e9 100644
--- a/server/service/global_schedule.go
+++ b/server/service/global_schedule.go
@@ -1,9 +1,10 @@
 package service
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/fleetdm/fleet/v4/server/fleet"
 	"github.com/fleetdm/fleet/v4/server/ptr"
 )
 
@@ -34,11 +35,13 @@ func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fle
 		GlobalSchedule: gp,
 	}, nil
 }
 
 func (svc *Service) GetGlobalScheduledQueries(ctx context.Context, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.Pack{
+		Type: ptr.String("global"),
+	}, fleet.ActionRead); err != nil {
 		return nil, err
 	}
 
 	gp, err := svc.ds.EnsureGlobalPack(ctx)
 	if err != nil {
@@ -86,11 +89,13 @@ func globalScheduleQueryEndpoint(ctx context.Context, request interface{}, svc f
 	}
 	return globalScheduleQueryResponse{Scheduled: scheduled}, nil
 }
 
 func (svc *Service) GlobalScheduleQuery(ctx context.Context, sq *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.Pack{
+		Type: ptr.String("global"),
+	}, fleet.ActionRead); err != nil {
 		return nil, err
 	}
 
 	gp, err := svc.ds.EnsureGlobalPack(ctx)
 	if err != nil {
@@ -129,11 +134,13 @@ func modifyGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc
 		Scheduled: sq,
 	}, nil
 }
 
 func (svc *Service) ModifyGlobalScheduledQueries(ctx context.Context, id uint, query fleet.ScheduledQueryPayload) (*fleet.ScheduledQuery, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionWrite); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.Pack{
+		Type: ptr.String("global"),
+	}, fleet.ActionWrite); err != nil {
 		return nil, err
 	}
 
 	gp, err := svc.ds.EnsureGlobalPack(ctx)
 	if err != nil {
@@ -168,11 +175,25 @@ func deleteGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc
 
 	return deleteGlobalScheduleResponse{}, nil
 }
 
 func (svc *Service) DeleteGlobalScheduledQueries(ctx context.Context, id uint) error {
-	if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionWrite); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.Pack{
+		Type: ptr.String("global"),
+	}, fleet.ActionWrite); err != nil {
 		return err
 	}
 
+	globalPack, err := svc.ds.EnsureGlobalPack(ctx)
+	if err != nil {
+		return err
+	}
+	scheduledQuery, err := svc.ds.ScheduledQuery(ctx, id)
+	if err != nil {
+		return err
+	}
+	if scheduledQuery.PackID != globalPack.ID {
+		return fmt.Errorf("scheduled query %d is not global", id)
+	}
+
 	return svc.DeleteScheduledQuery(ctx, id)
 }
diff --git a/server/service/global_schedule_test.go b/server/service/global_schedule_test.go
index 655d41ec6..024121389 100644
--- a/server/service/global_schedule_test.go
+++ b/server/service/global_schedule_test.go
@@ -38,44 +38,44 @@ func TestGlobalScheduleAuth(t *testing.T) {
 		user            *fleet.User
 		shouldFailWrite bool
 		shouldFailRead  bool
 	}{
 		{
-			"global admin",
-			&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
-			false,
-			false,
+			name:            "global admin",
+			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
+			shouldFailWrite: false,
+			shouldFailRead:  false,
 		},
 		{
-			"global maintainer",
-			&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
-			false,
-			false,
+			name:            "global maintainer",
+			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
+			shouldFailWrite: false,
+			shouldFailRead:  false,
 		},
 		{
-			"global observer",
-			&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
-			true,
-			true,
+			name:            "global observer",
+			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
+			shouldFailWrite: true,
+			shouldFailRead:  false,
 		},
 		{
-			"team admin",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
-			true,
-			false,
+			name:            "team admin",
+			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
+			shouldFailWrite: true,
+			shouldFailRead:  false,
 		},
 		{
-			"team maintainer",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
-			true,
-			false,
+			name:            "team maintainer",
+			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
+			shouldFailWrite: true,
+			shouldFailRead:  false,
 		},
 		{
-			"team observer",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
-			true,
-			true,
+			name:            "team observer",
+			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
+			shouldFailWrite: true,
+			shouldFailRead:  false,
 		},
 	}
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
 			ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
diff --git a/server/service/hosts.go b/server/service/hosts.go
index 403b8758f..664dd0062 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -5,11 +5,10 @@ import (
 	"fmt"
 	"net/http"
 	"time"
 
 	"github.com/fleetdm/fleet/v4/server/contexts/authz"
-	authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
 	"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
 	"github.com/fleetdm/fleet/v4/server/contexts/logging"
 	"github.com/fleetdm/fleet/v4/server/contexts/viewer"
 	"github.com/fleetdm/fleet/v4/server/fleet"
 	"github.com/gocarina/gocsv"
@@ -273,11 +272,11 @@ func getHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service
 
 	return getHostResponse{Host: resp}, nil
 }
 
 func (svc *Service) GetHost(ctx context.Context, id uint) (*fleet.HostDetail, error) {
-	alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken)
+	alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken)
 	if !alreadyAuthd {
 		// First ensure the user has access to list hosts, then check the specific
 		// host once team_id is loaded.
 		if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
 			return nil, err
@@ -558,11 +557,11 @@ func refetchHostEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
 	}
 	return refetchHostResponse{}, nil
 }
 
 func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
-	if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) {
+	if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
 		if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
 			return err
 		}
 
 		host, err := svc.ds.HostLite(ctx, id)
@@ -674,11 +673,11 @@ func listHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc
 	}
 	return listHostDeviceMappingResponse{HostID: req.ID, DeviceMapping: dms}, nil
 }
 
 func (svc *Service) ListHostDeviceMapping(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
-	if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) {
+	if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
 		if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
 			return nil, err
 		}
 
 		host, err := svc.ds.HostLite(ctx, id)
@@ -718,11 +717,11 @@ func getMacadminsDataEndpoint(ctx context.Context, request interface{}, svc flee
 	}
 	return getMacadminsDataResponse{Macadmins: data}, nil
 }
 
 func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.MacadminsData, error) {
-	if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) {
+	if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
 		if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
 			return nil, err
 		}
 
 		host, err := svc.ds.HostLite(ctx, id)
@@ -930,7 +929,6 @@ func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *stri
 	if err != nil {
 		return nil, err
 	}
 
 	return osVersions, nil
-
 }
diff --git a/server/service/scheduled_queries_test.go b/server/service/scheduled_queries_test.go
index ac73d7b73..28153ce6d 100644
--- a/server/service/scheduled_queries_test.go
+++ b/server/service/scheduled_queries_test.go
@@ -41,44 +41,47 @@ func TestScheduledQueriesAuth(t *testing.T) {
 		user            *fleet.User
 		shouldFailWrite bool
 		shouldFailRead  bool
 	}{
 		{
-			"global admin",
-			&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
-			false,
-			false,
+			name:            "global admin",
+			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
+			shouldFailWrite: false,
+			shouldFailRead:  false,
 		},
 		{
-			"global maintainer",
-			&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
-			false,
-			false,
+			name:            "global maintainer",
+			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
+			shouldFailWrite: false,
+			shouldFailRead:  false,
 		},
 		{
-			"global observer",
-			&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
-			true,
-			true,
+			name:            "global observer",
+			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
+			shouldFailWrite: true,
+			shouldFailRead:  true,
 		},
+		// Team users cannot read or write scheduled queries using the below service APIs.
+		// Team users must use the "Team" endpoints (GetTeamScheduledQueries, TeamScheduleQuery,
+		// ModifyTeamScheduledQueries and DeleteTeamScheduledQueries).
 		{
-			"team admin",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
-			true,
-			false,
+			name:            "team admin",
+			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
+			shouldFailWrite: true,
+			shouldFailRead:  true,
 		},
 		{
-			"team maintainer",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
-			true,
-			false,
+			name:            "team maintainer",
+			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
+			shouldFailWrite: true,
+			shouldFailRead:  true,
 		},
 		{
-			"team observer",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
-			true,
-			true,
+			name:            "team observer",
+			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
+			shouldFailWrite: true,
+			shouldFailRead:  true,
 		},
 	}
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
 			ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
diff --git a/server/service/software.go b/server/service/software.go
index 845160f33..f8ae36023 100644
--- a/server/service/software.go
+++ b/server/service/software.go
@@ -42,11 +42,13 @@ func listSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Se
 	}
 	return listResp, nil
 }
 
 func (svc Service) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.Software{}, fleet.ActionRead); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
+		TeamID: opt.TeamID,
+	}, fleet.ActionRead); err != nil {
 		return nil, err
 	}
 
 	// default sort order to hosts_count descending
 	if opt.OrderKey == "" {
@@ -80,11 +82,13 @@ func countSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.S
 	}
 	return countSoftwareResponse{Count: count}, nil
 }
 
 func (svc Service) CountSoftware(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.Software{}, fleet.ActionRead); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
+		TeamID: opt.TeamID,
+	}, fleet.ActionRead); err != nil {
 		return 0, err
 	}
 
 	return svc.ds.CountSoftware(ctx, opt)
 }
diff --git a/server/service/software_test.go b/server/service/software_test.go
index b3e3f4933..f5b390d30 100644
--- a/server/service/software_test.go
+++ b/server/service/software_test.go
@@ -21,11 +21,15 @@ func TestService_ListSoftware(t *testing.T) {
 		calledWithTeamID = opt.TeamID
 		calledWithOpt = opt
 		return []fleet.Software{}, nil
 	}
 
-	user := &fleet.User{ID: 3, Email: "foo@bar.com", GlobalRole: ptr.String(fleet.RoleObserver)}
+	user := &fleet.User{
+		ID:         3,
+		Email:      "foo@bar.com",
+		GlobalRole: ptr.String(fleet.RoleAdmin),
+	}
 
 	svc := newTestService(t, ds, nil, nil)
 	ctx := context.Background()
 	ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
 
@@ -46,5 +50,148 @@ func TestService_ListSoftware(t *testing.T) {
 	assert.True(t, ds.ListSoftwareFuncInvoked)
 	assert.Nil(t, calledWithTeamID)
 	assert.Equal(t, fleet.ListOptions{PerPage: 11, Page: 2, OrderKey: "id", OrderDirection: fleet.OrderAscending}, calledWithOpt.ListOptions)
 	assert.True(t, calledWithOpt.WithHostCounts)
 }
+
+func TestServiceSoftwareInventoryAuth(t *testing.T) {
+	ds := new(mock.Store)
+
+	ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) {
+		return []fleet.Software{}, nil
+	}
+	ds.CountSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) {
+		return 0, nil
+	}
+	svc := newTestService(t, ds, nil, nil)
+
+	for _, tc := range []struct {
+		name                 string
+		user                 *fleet.User
+		shouldFailGlobalRead bool
+		shouldFailTeamRead   bool
+	}{
+		{
+			name: "global-admin",
+			user: &fleet.User{
+				ID:         1,
+				GlobalRole: ptr.String(fleet.RoleAdmin),
+			},
+			shouldFailGlobalRead: false,
+			shouldFailTeamRead:   false,
+		},
+		{
+			name: "global-maintainer",
+			user: &fleet.User{
+				ID:         1,
+				GlobalRole: ptr.String(fleet.RoleMaintainer),
+			},
+			shouldFailGlobalRead: false,
+			shouldFailTeamRead:   false,
+		},
+		{
+			name: "global-observer",
+			user: &fleet.User{
+				ID:         1,
+				GlobalRole: ptr.String(fleet.RoleObserver),
+			},
+			shouldFailGlobalRead: false,
+			shouldFailTeamRead:   false,
+		},
+		{
+			name: "team-admin-belongs-to-team",
+			user: &fleet.User{
+				ID: 1,
+				Teams: []fleet.UserTeam{{
+					Team: fleet.Team{ID: 1},
+					Role: fleet.RoleAdmin,
+				}},
+			},
+			shouldFailGlobalRead: true,
+			shouldFailTeamRead:   false,
+		},
+		{
+			name: "team-maintainer-belongs-to-team",
+			user: &fleet.User{
+				ID: 1,
+				Teams: []fleet.UserTeam{{
+					Team: fleet.Team{ID: 1},
+					Role: fleet.RoleMaintainer,
+				}},
+			},
+			shouldFailGlobalRead: true,
+			shouldFailTeamRead:   false,
+		},
+		{
+			name: "team-observer-belongs-to-team",
+			user: &fleet.User{
+				ID: 1,
+				Teams: []fleet.UserTeam{{
+					Team: fleet.Team{ID: 1},
+					Role: fleet.RoleObserver,
+				}},
+			},
+			shouldFailGlobalRead: true,
+			shouldFailTeamRead:   false,
+		},
+		{
+			name: "team-admin-does-not-belong-to-team",
+			user: &fleet.User{
+				ID: 1,
+				Teams: []fleet.UserTeam{{
+					Team: fleet.Team{ID: 2},
+					Role: fleet.RoleAdmin,
+				}},
+			},
+			shouldFailGlobalRead: true,
+			shouldFailTeamRead:   true,
+		},
+		{
+			name: "team-maintainer-does-not-belong-to-team",
+			user: &fleet.User{
+				ID: 1,
+				Teams: []fleet.UserTeam{{
+					Team: fleet.Team{ID: 2},
+					Role: fleet.RoleMaintainer,
+				}},
+			},
+			shouldFailGlobalRead: true,
+			shouldFailTeamRead:   true,
+		},
+		{
+			name: "team-observer-does-not-belong-to-team",
+			user: &fleet.User{
+				ID: 1,
+				Teams: []fleet.UserTeam{{
+					Team: fleet.Team{ID: 2},
+					Role: fleet.RoleObserver,
+				}},
+			},
+			shouldFailGlobalRead: true,
+			shouldFailTeamRead:   true,
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tc.user})
+
+			// List all software.
+			_, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{})
+			checkAuthErr(t, tc.shouldFailGlobalRead, err)
+
+			// Count all software.
+			_, err = svc.CountSoftware(ctx, fleet.SoftwareListOptions{})
+			checkAuthErr(t, tc.shouldFailGlobalRead, err)
+
+			// List software for a team.
+			_, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{
+				TeamID: ptr.Uint(1),
+			})
+			checkAuthErr(t, tc.shouldFailTeamRead, err)
+
+			// Count software for a team.
+			_, err = svc.CountSoftware(ctx, fleet.SoftwareListOptions{
+				TeamID: ptr.Uint(1),
+			})
+			checkAuthErr(t, tc.shouldFailTeamRead, err)
+		})
+	}
+}
diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go
index 46de4c4ae..b281ac48e 100644
--- a/server/service/team_schedule.go
+++ b/server/service/team_schedule.go
@@ -1,9 +1,10 @@
 package service
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/fleetdm/fleet/v4/server/fleet"
 	"github.com/fleetdm/fleet/v4/server/ptr"
 	"gopkg.in/guregu/null.v3"
 )
@@ -34,11 +35,13 @@ func getTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet
 	}
 	return resp, nil
 }
 
 func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.Pack{TeamIDs: []uint{teamID}}, fleet.ActionRead); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.Pack{
+		Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
+	}, fleet.ActionRead); err != nil {
 		return nil, err
 	}
 
 	gp, err := svc.ds.EnsureTeamPack(ctx, teamID)
 	if err != nil {
@@ -90,18 +93,19 @@ func teamScheduleQueryEndpoint(ctx context.Context, request interface{}, svc fle
 		Shard:    nullIntToPtrUint(req.Shard),
 	})
 	if err != nil {
 		return teamScheduleQueryResponse{Err: err}, nil
 	}
-	_ = resp
 	return teamScheduleQueryResponse{
 		Scheduled: resp,
 	}, nil
 }
 
 func (svc Service) TeamScheduleQuery(ctx context.Context, teamID uint, q *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.Pack{TeamIDs: []uint{teamID}}, fleet.ActionWrite); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.Pack{
+		Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
+	}, fleet.ActionWrite); err != nil {
 		return nil, err
 	}
 
 	gp, err := svc.ds.EnsureTeamPack(ctx, teamID)
 	if err != nil {
@@ -138,11 +142,13 @@ func modifyTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fl
 	_ = resp
 	return modifyTeamScheduleResponse{}, nil
 }
 
 func (svc Service) ModifyTeamScheduledQueries(ctx context.Context, teamID uint, scheduledQueryID uint, query fleet.ScheduledQueryPayload) (*fleet.ScheduledQuery, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.Pack{TeamIDs: []uint{teamID}}, fleet.ActionWrite); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.Pack{
+		Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
+	}, fleet.ActionWrite); err != nil {
 		return nil, err
 	}
 
 	gp, err := svc.ds.EnsureTeamPack(ctx, teamID)
 	if err != nil {
@@ -178,10 +184,12 @@ func deleteTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fl
 	}
 	return deleteTeamScheduleResponse{}, nil
 }
 
 func (svc Service) DeleteTeamScheduledQueries(ctx context.Context, teamID uint, scheduledQueryID uint) error {
-	if err := svc.authz.Authorize(ctx, &fleet.Pack{TeamIDs: []uint{teamID}}, fleet.ActionWrite); err != nil {
+	if err := svc.authz.Authorize(ctx, &fleet.Pack{
+		Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
+	}, fleet.ActionWrite); err != nil {
 		return err
 	}
 	return svc.ds.DeleteScheduledQuery(ctx, scheduledQueryID)
 }
diff --git a/server/service/team_schedule_test.go b/server/service/team_schedule_test.go
index bf5e363a5..9535a333f 100644
--- a/server/service/team_schedule_test.go
+++ b/server/service/team_schedule_test.go
@@ -1,9 +1,10 @@
 package service
 
 import (
 	"context"
+	"fmt"
 	"testing"
 
 	"github.com/fleetdm/fleet/v4/server/contexts/viewer"
 	"github.com/fleetdm/fleet/v4/server/fleet"
 	"github.com/fleetdm/fleet/v4/server/mock"
@@ -13,11 +14,14 @@ import (
 func TestTeamScheduleAuth(t *testing.T) {
 	ds := new(mock.Store)
 	svc := newTestService(t, ds, nil, nil)
 
 	ds.EnsureTeamPackFunc = func(ctx context.Context, teamID uint) (*fleet.Pack, error) {
-		return &fleet.Pack{ID: 999}, nil
+		return &fleet.Pack{
+			ID:   999,
+			Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
+		}, nil
 	}
 	ds.ListScheduledQueriesInPackWithStatsFunc = func(ctx context.Context, id uint, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) {
 		return nil, nil
 	}
 	ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
@@ -74,11 +78,11 @@ func TestTeamScheduleAuth(t *testing.T) {
 		},
 		{
 			"team observer, belongs to team",
 			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
 			true,
-			true,
+			false,
 		},
 		{
 			"team maintainer, DOES NOT belong to team",
 			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
 			true,
diff --git a/server/service/users.go b/server/service/users.go
index 453383040..3b61b4787 100644
--- a/server/service/users.go
+++ b/server/service/users.go
@@ -9,10 +9,11 @@ import (
 	"net/http"
 	"time"
 
 	"github.com/fleetdm/fleet/v4/server"
 	"github.com/fleetdm/fleet/v4/server/authz"
+	authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
 	"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
 	"github.com/fleetdm/fleet/v4/server/contexts/viewer"
 	"github.com/fleetdm/fleet/v4/server/fleet"
 	"github.com/fleetdm/fleet/v4/server/mail"
 	"github.com/fleetdm/fleet/v4/server/ptr"
@@ -138,11 +139,15 @@ func listUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Servi
 	}
 	return resp, nil
 }
 
 func (svc *Service) ListUsers(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.User{}, fleet.ActionRead); err != nil {
+	user := &fleet.User{}
+	if opt.TeamID != 0 {
+		user.Teams = []fleet.UserTeam{{Team: fleet.Team{ID: opt.TeamID}}}
+	}
+	if err := svc.authz.Authorize(ctx, user, fleet.ActionRead); err != nil {
 		return nil, err
 	}
 
 	return svc.ds.ListUsers(ctx, opt)
 }
@@ -214,16 +219,31 @@ func getUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service
 		}
 	}
 	return getUserResponse{User: user, AvailableTeams: availableTeams}, nil
 }
 
+// setAuthCheckedOnPreAuthErr can be used to set the authentication as checked
+// in case of errors that happened before an auth check can be performed.
+// Otherwise the endpoints return a "authentication skipped" error instead of
+// the actual returned error.
+func setAuthCheckedOnPreAuthErr(ctx context.Context) {
+	if az, ok := authz_ctx.FromContext(ctx); ok {
+		az.SetChecked()
+	}
+}
+
 func (svc *Service) User(ctx context.Context, id uint) (*fleet.User, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.User{ID: id}, fleet.ActionRead); err != nil {
-		return nil, err
+	user, err := svc.ds.UserByID(ctx, id)
+	if err != nil {
+		setAuthCheckedOnPreAuthErr(ctx)
+		return nil, ctxerr.Wrap(ctx, err)
 	}
 
-	return svc.ds.UserByID(ctx, id)
+	if err := svc.authz.Authorize(ctx, user, fleet.ActionRead); err != nil {
+		return nil, err
+	}
+	return user, nil
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Modify User
 ////////////////////////////////////////////////////////////////////////////////
@@ -249,16 +269,13 @@ func modifyUserEndpoint(ctx context.Context, request interface{}, svc fleet.Serv
 
 	return modifyUserResponse{User: user}, nil
 }
 
 func (svc *Service) ModifyUser(ctx context.Context, userID uint, p fleet.UserPayload) (*fleet.User, error) {
-	if err := svc.authz.Authorize(ctx, &fleet.User{}, fleet.ActionRead); err != nil {
-		return nil, err
-	}
-
 	user, err := svc.User(ctx, userID)
 	if err != nil {
+		setAuthCheckedOnPreAuthErr(ctx)
 		return nil, err
 	}
 
 	if err := svc.authz.Authorize(ctx, user, fleet.ActionWrite); err != nil {
 		return nil, err
@@ -324,21 +341,27 @@ func (svc *Service) ModifyUser(ctx context.Context, userID uint, p fleet.UserPay
 
 	currentUser := authz.UserFromContext(ctx)
 
 	if p.GlobalRole != nil && *p.GlobalRole != "" {
 		if currentUser.GlobalRole == nil {
-			return nil, ctxerr.New(ctx, "Cannot edit global role as a team member")
+			return nil, authz.ForbiddenWithInternal(
+				"cannot edit global role as a team member",
+				currentUser, user, fleet.ActionWriteRole,
+			)
 		}
 
 		if p.Teams != nil && len(*p.Teams) > 0 {
 			return nil, fleet.NewInvalidArgumentError("teams", "may not be specified with global_role")
 		}
 		user.GlobalRole = p.GlobalRole
 		user.Teams = []fleet.UserTeam{}
 	} else if p.Teams != nil {
 		if !isAdminOfTheModifiedTeams(currentUser, user.Teams, *p.Teams) {
-			return nil, ctxerr.New(ctx, "Cannot modify teams in that way")
+			return nil, authz.ForbiddenWithInternal(
+				"cannot modify teams in that way",
+				currentUser, user, fleet.ActionWriteRole,
+			)
 		}
 		user.Teams = *p.Teams
 		user.GlobalRole = nil
 	}
 
@@ -377,14 +400,18 @@ func deleteUserEndpoint(ctx context.Context, request interface{}, svc fleet.Serv
 	}
 	return deleteUserResponse{}, nil
 }
 
 func (svc *Service) DeleteUser(ctx context.Context, id uint) error {
-	if err := svc.authz.Authorize(ctx, &fleet.User{ID: id}, fleet.ActionWrite); err != nil {
+	user, err := svc.ds.UserByID(ctx, id)
+	if err != nil {
+		setAuthCheckedOnPreAuthErr(ctx)
+		return ctxerr.Wrap(ctx, err)
+	}
+	if err := svc.authz.Authorize(ctx, user, fleet.ActionWrite); err != nil {
 		return err
 	}
-
 	return svc.ds.DeleteUser(ctx, id)
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Require Password Reset
@@ -614,39 +641,55 @@ func (svc *Service) ChangeUserEmail(ctx context.Context, token string) (string,
 	}
 
 	return svc.ds.ConfirmPendingEmailChange(ctx, vc.UserID(), token)
 }
 
+// isAdminOfTheModifiedTeams checks whether the current user is allowed to modify the user
+// roles in the teams.
+//
+// TODO: End-goal is to move all this logic to policy.rego.
 func isAdminOfTheModifiedTeams(currentUser *fleet.User, originalUserTeams, newUserTeams []fleet.UserTeam) bool {
-	// If the user is of the right global role, then they can modify the teams
-	if currentUser.GlobalRole != nil && (*currentUser.GlobalRole == fleet.RoleAdmin || *currentUser.GlobalRole == fleet.RoleMaintainer) {
+	// Global admins can modify all user teams roles.
+	if currentUser.GlobalRole != nil && *currentUser.GlobalRole == fleet.RoleAdmin {
 		return true
 	}
 
-	// otherwise, gather the resulting teams
-	resultingTeams := make(map[uint]string)
+	// Otherwise, make a map of the original and resulting teams.
+	newTeams := make(map[uint]string)
 	for _, team := range newUserTeams {
-		resultingTeams[team.ID] = team.Role
+		newTeams[team.ID] = team.Role
+	}
+	originalTeams := make(map[uint]struct{})
+	for _, team := range originalUserTeams {
+		originalTeams[team.ID] = struct{}{}
 	}
 
-	// and see which ones were removed or changed from the original
+	// See which ones were removed or changed from the original.
 	teamsAffected := make(map[uint]struct{})
 	for _, team := range originalUserTeams {
-		if resultingTeams[team.ID] != team.Role {
+		if newTeams[team.ID] != team.Role {
+			teamsAffected[team.ID] = struct{}{}
+		}
+	}
+
+	// See which ones of the new are not in the original.
+	for _, team := range newUserTeams {
+		if _, ok := originalTeams[team.ID]; !ok {
 			teamsAffected[team.ID] = struct{}{}
 		}
 	}
 
-	// then gather the teams the current user is admin for
+	// Then gather the teams the current user is admin for.
 	currentUserTeamAdmin := make(map[uint]struct{})
 	for _, team := range currentUser.Teams {
 		if team.Role == fleet.RoleAdmin {
 			currentUserTeamAdmin[team.ID] = struct{}{}
 		}
 	}
 
-	// and let's check that the teams that were either removed or changed are also teams this user is an admin of
+	// And finally, let's check that the teams that were either removed
+	// or changed are also teams this user is an admin of.
 	for teamID := range teamsAffected {
 		if _, ok := currentUserTeamAdmin[teamID]; !ok {
 			return false
 		}
 	}
diff --git a/server/service/users_test.go b/server/service/users_test.go
index c68aeda26..b7597a008 100644
--- a/server/service/users_test.go
+++ b/server/service/users_test.go
@@ -5,10 +5,11 @@ import (
 	"database/sql"
 	"errors"
 	"testing"
 	"time"
 
+	"github.com/fleetdm/fleet/v4/server/authz"
 	"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
 	"github.com/fleetdm/fleet/v4/server/contexts/viewer"
 	"github.com/fleetdm/fleet/v4/server/datastore/mysql"
 	"github.com/fleetdm/fleet/v4/server/fleet"
 	"github.com/fleetdm/fleet/v4/server/mock"
@@ -39,22 +40,31 @@ func TestUserAuth(t *testing.T) {
 		return nil
 	}
 	ds.InviteByEmailFunc = func(ctx context.Context, email string) (*fleet.Invite, error) {
 		return nil, errors.New("AA")
 	}
+
+	userTeamMaintainerID := uint(999)
+	userGlobalMaintainerID := uint(888)
+	var self *fleet.User // to be set by tests
 	ds.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
-		if id == 999 {
+		switch id {
+		case userTeamMaintainerID:
 			return &fleet.User{
-				ID:    999,
+				ID:    userTeamMaintainerID,
 				Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}},
 			}, nil
+		case userGlobalMaintainerID:
+			return &fleet.User{
+				ID:         userGlobalMaintainerID,
+				GlobalRole: ptr.String(fleet.RoleMaintainer),
+			}, nil
+		default:
+			return self, nil
 		}
-		return &fleet.User{
-			ID:         888,
-			GlobalRole: ptr.String(fleet.RoleMaintainer),
-		}, nil
 	}
+
 	ds.SaveUserFunc = func(ctx context.Context, user *fleet.User) error {
 		return nil
 	}
 	ds.ListUsersFunc = func(ctx context.Context, opts fleet.UserListOptions) ([]*fleet.User, error) {
 		return nil, nil
@@ -68,96 +78,281 @@ func TestUserAuth(t *testing.T) {
 	ds.ListSessionsForUserFunc = func(ctx context.Context, id uint) ([]*fleet.Session, error) {
 		return nil, nil
 	}
 
 	testCases := []struct {
-		name                  string
-		user                  *fleet.User
+		name string
+		user *fleet.User
+
 		shouldFailGlobalWrite bool
 		shouldFailTeamWrite   bool
-		shouldFailRead        bool
-		shouldFailDeleteReset bool
+
+		shouldFailWriteRoleGlobalToGlobal    bool
+		shouldFailWriteRoleGlobalToTeam      bool
+		shouldFailWriteRoleTeamToAnotherTeam bool
+		shouldFailWriteRoleTeamToGlobal      bool
+
+		shouldFailWriteRoleOwnDomain bool
+
+		shouldFailGlobalRead bool
+		shouldFailTeamRead   bool
+
+		shouldFailGlobalDelete bool
+		shouldFailTeamDelete   bool
+
+		shouldFailGlobalPasswordReset bool
+		shouldFailTeamPasswordReset   bool
+
+		shouldFailGlobalChangePassword bool
+		shouldFailTeamChangePassword   bool
+
+		shouldFailListAll  bool
+		shouldFailListTeam bool
 	}{
 		{
-			"global admin",
-			&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
-			false,
-			false,
-			false,
-			false,
+			name:                                 "global admin",
+			user:                                 &fleet.User{ID: 1000, GlobalRole: ptr.String(fleet.RoleAdmin)},
+			shouldFailGlobalWrite:                false,
+			shouldFailTeamWrite:                  false,
+			shouldFailWriteRoleGlobalToGlobal:    false,
+			shouldFailWriteRoleGlobalToTeam:      false,
+			shouldFailWriteRoleTeamToAnotherTeam: false,
+			shouldFailWriteRoleTeamToGlobal:      false,
+			shouldFailWriteRoleOwnDomain:         false,
+			shouldFailGlobalRead:                 false,
+			shouldFailTeamRead:                   false,
+			shouldFailGlobalDelete:               false,
+			shouldFailTeamDelete:                 false,
+			shouldFailGlobalPasswordReset:        false,
+			shouldFailTeamPasswordReset:          false,
+			shouldFailGlobalChangePassword:       false,
+			shouldFailTeamChangePassword:         false,
+			shouldFailListAll:                    false,
+			shouldFailListTeam:                   false,
 		},
 		{
-			"global maintainer",
-			&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
-			true,
-			true,
-			false,
-			true,
+			name:                                 "global maintainer",
+			user:                                 &fleet.User{ID: 1000, GlobalRole: ptr.String(fleet.RoleMaintainer)},
+			shouldFailGlobalWrite:                true,
+			shouldFailTeamWrite:                  true,
+			shouldFailWriteRoleGlobalToGlobal:    true,
+			shouldFailWriteRoleGlobalToTeam:      true,
+			shouldFailWriteRoleTeamToAnotherTeam: true,
+			shouldFailWriteRoleTeamToGlobal:      true,
+			shouldFailWriteRoleOwnDomain:         true,
+			shouldFailGlobalRead:                 true,
+			shouldFailTeamRead:                   true,
+			shouldFailGlobalDelete:               true,
+			shouldFailTeamDelete:                 true,
+			shouldFailGlobalPasswordReset:        true,
+			shouldFailTeamPasswordReset:          true,
+			shouldFailGlobalChangePassword:       true,
+			shouldFailTeamChangePassword:         true,
+			shouldFailListAll:                    true,
+			shouldFailListTeam:                   true,
 		},
 		{
-			"global observer",
-			&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
-			true,
-			true,
-			false,
-			true,
+			name:                                 "global observer",
+			user:                                 &fleet.User{ID: 1000, GlobalRole: ptr.String(fleet.RoleObserver)},
+			shouldFailGlobalWrite:                true,
+			shouldFailTeamWrite:                  true,
+			shouldFailWriteRoleGlobalToGlobal:    true,
+			shouldFailWriteRoleGlobalToTeam:      true,
+			shouldFailWriteRoleTeamToAnotherTeam: true,
+			shouldFailWriteRoleTeamToGlobal:      true,
+			shouldFailWriteRoleOwnDomain:         true,
+			shouldFailGlobalRead:                 true,
+			shouldFailTeamRead:                   true,
+			shouldFailGlobalDelete:               true,
+			shouldFailTeamDelete:                 true,
+			shouldFailGlobalPasswordReset:        true,
+			shouldFailTeamPasswordReset:          true,
+			shouldFailGlobalChangePassword:       true,
+			shouldFailTeamChangePassword:         true,
+			shouldFailListAll:                    true,
+			shouldFailListTeam:                   true,
 		},
 		{
-			"team admin, belongs to team",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
-			true,
-			false,
-			false,
-			true,
+			name:                                 "team admin, belongs to team",
+			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
+			shouldFailGlobalWrite:                true,
+			shouldFailTeamWrite:                  false,
+			shouldFailWriteRoleGlobalToGlobal:    true,
+			shouldFailWriteRoleGlobalToTeam:      true,
+			shouldFailWriteRoleTeamToAnotherTeam: true,
+			shouldFailWriteRoleTeamToGlobal:      true,
+			shouldFailWriteRoleOwnDomain:         false,
+			shouldFailGlobalRead:                 true,
+			shouldFailTeamRead:                   false,
+			shouldFailGlobalDelete:               true,
+			shouldFailTeamDelete:                 false,
+			shouldFailGlobalPasswordReset:        true,
+			shouldFailTeamPasswordReset:          true,
+			shouldFailGlobalChangePassword:       true,
+			shouldFailTeamChangePassword:         true,
+			shouldFailListAll:                    true,
+			shouldFailListTeam:                   false,
 		},
 		{
-			"team maintainer, belongs to team",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
-			true,
-			true,
-			false,
-			true,
+			name:                                 "team maintainer, belongs to team",
+			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
+			shouldFailGlobalWrite:                true,
+			shouldFailTeamWrite:                  true,
+			shouldFailWriteRoleGlobalToGlobal:    true,
+			shouldFailWriteRoleGlobalToTeam:      true,
+			shouldFailWriteRoleTeamToAnotherTeam: true,
+			shouldFailWriteRoleTeamToGlobal:      true,
+			shouldFailWriteRoleOwnDomain:         true,
+			shouldFailGlobalRead:                 true,
+			shouldFailTeamRead:                   true,
+			shouldFailGlobalDelete:               true,
+			shouldFailTeamDelete:                 true,
+			shouldFailGlobalPasswordReset:        true,
+			shouldFailTeamPasswordReset:          true,
+			shouldFailGlobalChangePassword:       true,
+			shouldFailTeamChangePassword:         true,
+			shouldFailListAll:                    true,
+			shouldFailListTeam:                   true,
 		},
 		{
-			"team observer, belongs to team",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
-			true,
-			true,
-			false,
-			true,
+			name:                                 "team observer, belongs to team",
+			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
+			shouldFailGlobalWrite:                true,
+			shouldFailTeamWrite:                  true,
+			shouldFailWriteRoleGlobalToGlobal:    true,
+			shouldFailWriteRoleGlobalToTeam:      true,
+			shouldFailWriteRoleTeamToAnotherTeam: true,
+			shouldFailWriteRoleTeamToGlobal:      true,
+			shouldFailWriteRoleOwnDomain:         true,
+			shouldFailGlobalRead:                 true,
+			shouldFailTeamRead:                   true,
+			shouldFailGlobalDelete:               true,
+			shouldFailTeamDelete:                 true,
+			shouldFailGlobalPasswordReset:        true,
+			shouldFailTeamPasswordReset:          true,
+			shouldFailGlobalChangePassword:       true,
+			shouldFailTeamChangePassword:         true,
+			shouldFailListAll:                    true,
+			shouldFailListTeam:                   true,
 		},
 		{
-			"team maintainer, DOES NOT belong to team",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
-			true,
-			true,
-			false,
-			true,
+			name:                                 "team maintainer, DOES NOT belong to team",
+			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
+			shouldFailGlobalWrite:                true,
+			shouldFailTeamWrite:                  true,
+			shouldFailWriteRoleGlobalToGlobal:    true,
+			shouldFailWriteRoleGlobalToTeam:      true,
+			shouldFailWriteRoleTeamToAnotherTeam: true,
+			shouldFailWriteRoleTeamToGlobal:      true,
+			shouldFailWriteRoleOwnDomain:         true,
+			shouldFailGlobalRead:                 true,
+			shouldFailTeamRead:                   true,
+			shouldFailGlobalDelete:               true,
+			shouldFailTeamDelete:                 true,
+			shouldFailGlobalPasswordReset:        true,
+			shouldFailTeamPasswordReset:          true,
+			shouldFailGlobalChangePassword:       true,
+			shouldFailTeamChangePassword:         true,
+			shouldFailListAll:                    true,
+			shouldFailListTeam:                   true,
 		},
 		{
-			"team admin, DOES NOT belong to team",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
-			true,
-			true,
-			false,
-			true,
+			name:                                 "team admin, DOES NOT belong to team",
+			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
+			shouldFailGlobalWrite:                true,
+			shouldFailTeamWrite:                  true,
+			shouldFailWriteRoleGlobalToGlobal:    true,
+			shouldFailWriteRoleGlobalToTeam:      true,
+			shouldFailWriteRoleTeamToAnotherTeam: true,
+			shouldFailWriteRoleTeamToGlobal:      true,
+			shouldFailWriteRoleOwnDomain:         false, // this is testing changing its own role in the team it belongs to.
+			shouldFailGlobalRead:                 true,
+			shouldFailTeamRead:                   true,
+			shouldFailGlobalDelete:               true,
+			shouldFailTeamDelete:                 true,
+			shouldFailGlobalPasswordReset:        true,
+			shouldFailTeamPasswordReset:          true,
+			shouldFailGlobalChangePassword:       true,
+			shouldFailTeamChangePassword:         true,
+			shouldFailListAll:                    true,
+			shouldFailListTeam:                   true,
 		},
 		{
-			"team observer, DOES NOT belong to team",
-			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
-			true,
-			true,
-			false,
-			true,
+			name:                                 "team observer, DOES NOT belong to team",
+			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
+			shouldFailGlobalWrite:                true,
+			shouldFailTeamWrite:                  true,
+			shouldFailWriteRoleGlobalToGlobal:    true,
+			shouldFailWriteRoleGlobalToTeam:      true,
+			shouldFailWriteRoleTeamToAnotherTeam: true,
+			shouldFailWriteRoleTeamToGlobal:      true,
+			shouldFailWriteRoleOwnDomain:         true,
+			shouldFailGlobalRead:                 true,
+			shouldFailTeamRead:                   true,
+			shouldFailGlobalDelete:               true,
+			shouldFailTeamDelete:                 true,
+			shouldFailGlobalPasswordReset:        true,
+			shouldFailTeamPasswordReset:          true,
+			shouldFailGlobalChangePassword:       true,
+			shouldFailTeamChangePassword:         true,
+			shouldFailListAll:                    true,
+			shouldFailListTeam:                   true,
 		},
 	}
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
 			ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
 
+			tt.user.SetPassword("p4ssw0rd.", 10, 10)
+
+			// To test a user reading/modifying itself.
+			u := *tt.user
+			self = &u
+
+			// A user can always read itself (read rego action).
+			_, err := svc.User(ctx, tt.user.ID)
+			require.NoError(t, err)
+
+			// A user can always write itself (write rego action).
+			_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{Name: ptr.String("Foo")})
+			require.NoError(t, err)
+
+			// A user can always change its own password (change_password rego action).
+			_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{Password: ptr.String("p4ssw0rd."), NewPassword: ptr.String("p4ssw0rd.3")})
+			require.NoError(t, err)
+
+			changeRole := func(role string) string {
+				switch role {
+				case fleet.RoleMaintainer:
+					return fleet.RoleAdmin // promote
+				case fleet.RoleAdmin:
+					return fleet.RoleMaintainer // demote
+				case fleet.RoleObserver:
+					return fleet.RoleAdmin // promote
+				default:
+					t.Fatalf("unknown role: %s", role)
+					return ""
+				}
+			}
+
+			// Test a user modifying its own role within its domain (write_role rego action).
+			if tt.user.GlobalRole != nil {
+				_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{GlobalRole: ptr.String(changeRole(*tt.user.GlobalRole))})
+				checkAuthErr(t, tt.shouldFailWriteRoleOwnDomain, err)
+			} else { // Team user
+				ownTeamDifferentRole := []fleet.UserTeam{
+					{
+						Team: fleet.Team{ID: tt.user.Teams[0].ID},
+						Role: changeRole(tt.user.Teams[0].Role),
+					},
+				}
+				_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{Teams: &ownTeamDifferentRole})
+				checkAuthErr(t, tt.shouldFailWriteRoleOwnDomain, err)
+			}
+
 			teams := []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}
-			_, err := svc.CreateUser(ctx, fleet.UserPayload{
+			_, err = svc.CreateUser(ctx, fleet.UserPayload{
 				Name:     ptr.String("Some Name"),
 				Email:    ptr.String("some@email.com"),
 				Password: ptr.String("passw0rd."),
 				Teams:    &teams,
 			})
@@ -169,33 +364,58 @@ func TestUserAuth(t *testing.T) {
 				Password:   ptr.String("passw0rd."),
 				GlobalRole: ptr.String(fleet.RoleAdmin),
 			})
 			checkAuthErr(t, tt.shouldFailGlobalWrite, err)
 
-			_, err = svc.ModifyUser(ctx, 999, fleet.UserPayload{Teams: &teams})
+			_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{Name: ptr.String("Foo")})
+			checkAuthErr(t, tt.shouldFailGlobalWrite, err)
+
+			_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{Name: ptr.String("Bar")})
 			checkAuthErr(t, tt.shouldFailTeamWrite, err)
 
-			_, err = svc.ModifyUser(ctx, 888, fleet.UserPayload{Teams: &teams})
-			checkAuthErr(t, tt.shouldFailGlobalWrite, err)
+			_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{GlobalRole: ptr.String(fleet.RoleMaintainer)})
+			checkAuthErr(t, tt.shouldFailWriteRoleGlobalToGlobal, err)
 
-			_, err = svc.ModifyUser(ctx, 888, fleet.UserPayload{GlobalRole: ptr.String(fleet.RoleMaintainer)})
-			checkAuthErr(t, tt.shouldFailGlobalWrite, err)
+			_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{Teams: &teams})
+			checkAuthErr(t, tt.shouldFailWriteRoleGlobalToTeam, err)
 
-			err = svc.DeleteUser(ctx, 999)
-			checkAuthErr(t, tt.shouldFailDeleteReset, err)
+			anotherTeams := []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}
+			_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{Teams: &anotherTeams})
+			checkAuthErr(t, tt.shouldFailWriteRoleTeamToAnotherTeam, err)
 
-			_, err = svc.RequirePasswordReset(ctx, 999, false)
-			checkAuthErr(t, tt.shouldFailDeleteReset, err)
+			_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{GlobalRole: ptr.String(fleet.RoleMaintainer)})
+			checkAuthErr(t, tt.shouldFailWriteRoleTeamToGlobal, err)
 
-			_, err = svc.ListUsers(ctx, fleet.UserListOptions{})
-			checkAuthErr(t, tt.shouldFailRead, err)
+			_, err = svc.User(ctx, userGlobalMaintainerID)
+			checkAuthErr(t, tt.shouldFailGlobalRead, err)
+
+			_, err = svc.User(ctx, userTeamMaintainerID)
+			checkAuthErr(t, tt.shouldFailTeamRead, err)
+
+			err = svc.DeleteUser(ctx, userGlobalMaintainerID)
+			checkAuthErr(t, tt.shouldFailGlobalDelete, err)
+
+			err = svc.DeleteUser(ctx, userTeamMaintainerID)
+			checkAuthErr(t, tt.shouldFailTeamDelete, err)
+
+			_, err = svc.RequirePasswordReset(ctx, userGlobalMaintainerID, false)
+			checkAuthErr(t, tt.shouldFailGlobalPasswordReset, err)
 
-			_, err = svc.User(ctx, 999)
-			checkAuthErr(t, tt.shouldFailRead, err)
+			_, err = svc.RequirePasswordReset(ctx, userTeamMaintainerID, false)
+			checkAuthErr(t, tt.shouldFailTeamPasswordReset, err)
 
-			_, err = svc.User(ctx, 888)
-			checkAuthErr(t, tt.shouldFailRead, err)
+			_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{NewPassword: ptr.String("passw0rd.2")})
+			checkAuthErr(t, tt.shouldFailGlobalChangePassword, err)
+
+			_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{NewPassword: ptr.String("passw0rd.2")})
+			checkAuthErr(t, tt.shouldFailTeamChangePassword, err)
+
+			_, err = svc.ListUsers(ctx, fleet.UserListOptions{})
+			checkAuthErr(t, tt.shouldFailListAll, err)
+
+			_, err = svc.ListUsers(ctx, fleet.UserListOptions{TeamID: 1})
+			checkAuthErr(t, tt.shouldFailListTeam, err)
 		})
 	}
 }
 
 func TestModifyUserEmail(t *testing.T) {
@@ -675,5 +895,408 @@ func TestAuthenticatedUser(t *testing.T) {
 	ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin1, Session: admin1Session})
 	user, err := svc.AuthenticatedUser(ctx)
 	assert.Nil(t, err)
 	assert.Equal(t, user, admin1)
 }
+
+func TestIsAdminOfTheModifiedTeams(t *testing.T) {
+	type teamWithRole struct {
+		teamID uint
+		role   string
+	}
+	type roles struct {
+		global *string
+		teams  []teamWithRole
+	}
+	for _, tc := range []struct {
+		name string
+		// actionUserRoles are the roles of the user executing the role change action.
+		actionUserRoles roles
+		// targetUserOriginalTeams are the original teams the target user belongs to.
+		targetUserOriginalTeams []teamWithRole
+		// targetUserNewTeams are the new teams the target user will be added to.
+		targetUserNewTeams []teamWithRole
+
+		expected bool
+	}{
+		{
+			name: "global-admin-allmighty",
+			actionUserRoles: roles{
+				global: ptr.String(fleet.RoleAdmin),
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 2,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "global-maintainer-cannot-modify-team-users",
+			actionUserRoles: roles{
+				global: ptr.String(fleet.RoleMaintainer),
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleMaintainer,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "team-admin-of-original-and-new",
+			actionUserRoles: roles{
+				teams: []teamWithRole{
+					{
+						teamID: 1,
+						role:   fleet.RoleAdmin,
+					},
+					{
+						teamID: 2,
+						role:   fleet.RoleAdmin,
+					},
+				},
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 2,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "team-admin-of-one-original-and-leave-other-team-unmodified",
+			actionUserRoles: roles{
+				teams: []teamWithRole{
+					{
+						teamID: 1,
+						role:   fleet.RoleMaintainer,
+					},
+					{
+						teamID: 2,
+						role:   fleet.RoleAdmin,
+					},
+				},
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleMaintainer,
+				},
+				{
+					teamID: 2,
+					role:   fleet.RoleMaintainer,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleMaintainer,
+				},
+				{
+					teamID: 2,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "team-admin-of-original-only",
+			actionUserRoles: roles{
+				teams: []teamWithRole{
+					{
+						teamID: 1,
+						role:   fleet.RoleAdmin,
+					},
+					{
+						teamID: 2,
+						role:   fleet.RoleMaintainer,
+					},
+				},
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 2,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "team-admin-of-new-only",
+			actionUserRoles: roles{
+				teams: []teamWithRole{
+					{
+						teamID: 1,
+						role:   fleet.RoleObserver,
+					},
+					{
+						teamID: 2,
+						role:   fleet.RoleAdmin,
+					},
+				},
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 2,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "team-admin-but-new-another-team-observer",
+			actionUserRoles: roles{
+				teams: []teamWithRole{
+					{
+						teamID: 1,
+						role:   fleet.RoleAdmin,
+					},
+				},
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+				{
+					teamID: 2,
+					role:   fleet.RoleObserver,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "team-admin-but-new-another-team-admin",
+			actionUserRoles: roles{
+				teams: []teamWithRole{
+					{
+						teamID: 1,
+						role:   fleet.RoleAdmin,
+					},
+				},
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+				{
+					teamID: 2,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "team-admin-but-original-another-team",
+			actionUserRoles: roles{
+				teams: []teamWithRole{
+					{
+						teamID: 1,
+						role:   fleet.RoleAdmin,
+					},
+				},
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 2,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "team-admin-but-change-role-another-team",
+			actionUserRoles: roles{
+				teams: []teamWithRole{
+					{
+						teamID: 1,
+						role:   fleet.RoleAdmin,
+					},
+				},
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+				{
+					teamID: 2,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+				{
+					teamID: 2,
+					role:   fleet.RoleMaintainer,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "team-admin-of-one-original-only",
+			actionUserRoles: roles{
+				teams: []teamWithRole{
+					{
+						teamID: 1,
+						role:   fleet.RoleMaintainer,
+					},
+					{
+						teamID: 2,
+						role:   fleet.RoleAdmin,
+					},
+				},
+			},
+			targetUserOriginalTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleMaintainer,
+				},
+				{
+					teamID: 2,
+					role:   fleet.RoleMaintainer,
+				},
+			},
+			targetUserNewTeams: []teamWithRole{
+				{
+					teamID: 1,
+					role:   fleet.RoleAdmin,
+				},
+				{
+					teamID: 2,
+					role:   fleet.RoleAdmin,
+				},
+			},
+			expected: false,
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			userTeamsFn := func(twr []teamWithRole) []fleet.UserTeam {
+				var userTeams []fleet.UserTeam
+				for _, ot := range twr {
+					userTeams = append(userTeams, fleet.UserTeam{
+						Team: fleet.Team{ID: ot.teamID},
+						Role: ot.role,
+					})
+				}
+				return userTeams
+			}
+
+			actionUserTeams := userTeamsFn(tc.actionUserRoles.teams)
+			originalUserTeams := userTeamsFn(tc.targetUserOriginalTeams)
+			newUserTeams := userTeamsFn(tc.targetUserNewTeams)
+
+			result := isAdminOfTheModifiedTeams(
+				&fleet.User{
+					GlobalRole: tc.actionUserRoles.global,
+					Teams:      actionUserTeams,
+				},
+				originalUserTeams,
+				newUserTeams,
+			)
+			require.Equal(t, tc.expected, result)
+		})
+	}
+}
+
+// TestAdminAddRoleOtherTeam is an explicit test to check that
+// that an admin cannot add itself to another team.
+func TestTeamAdminAddRoleOtherTeam(t *testing.T) {
+	ds := new(mock.Store)
+	svc := newTestService(t, ds, nil, nil)
+
+	// adminTeam2 is a team admin of team with ID=2.
+	adminTeam2 := &fleet.User{
+		ID: 1,
+		Teams: []fleet.UserTeam{
+			{
+				Team: fleet.Team{ID: 2},
+				Role: fleet.RoleAdmin,
+			},
+		},
+	}
+
+	ds.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
+		if id != 1 {
+			return nil, &notFoundError{}
+		}
+		return adminTeam2, nil
+	}
+	ds.SaveUserFunc = func(ctx context.Context, user *fleet.User) error {
+		return nil
+	}
+
+	ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: adminTeam2})
+	adminTeam2.SetPassword("p4ssw0rd.", 10, 10)
+
+	// adminTeam2 tries to add itself to team with ID=3 as admin.
+	_, err := svc.ModifyUser(ctx, adminTeam2.ID, fleet.UserPayload{
+		Teams: &[]fleet.UserTeam{
+			{
+				Team: fleet.Team{ID: 2},
+				Role: fleet.RoleAdmin,
+			},
+			{
+				Team: fleet.Team{ID: 3},
+				Role: fleet.RoleAdmin,
+			},
+		},
+	})
+	require.Equal(t, (&authz.Forbidden{}).Error(), err.Error())
+	require.False(t, ds.SaveUserFuncInvoked)
+}

Vulnerable code:         it("hides manages software automations when all teams selected", () => {
          cy.getAttached(".manage-software-page__header-wrap").within(() => {
            cy.getAttached(".Select").within(() => {
              cy.findByText(/all teams/i).should("exist");
            });
            cy.getAttached(".manage-software-page__header-wrap").within(() => {
              cy.findByRole("button", {
                name: /manage automations/i,
              }).should("not.exist");
            });
          });
        });
        it("hides manage automations button when all teams not selected", () => {
          cy.getAttached(".manage-software-page__header-wrap").within(() => {
            cy.getAttached(".Select").within(() => {
              cy.getAttached(".Select-control").click();
              cy.getAttached(".Select-menu-outer").within(() => {
                cy.findByText(/apples/i).should("exist");
              });
              cy.findByRole("button", {
                name: /manage automations/i,
              }).should("not.exist");
            });
          });
        });
      it("allows maintainer to manage automations", () => {
        cy.findByRole("button", { name: /manage automations/i }).click();
        cy.findByRole("button", { name: /cancel/i }).click();
      cy.findByRole("button", { name: /manage automations/i }).should(
        "not.exist"
      );
      cy.findByRole("button", { name: /add a policy/i }).should("not.exist");
      it("should restrict global maintainer from 'Manage automations' button", () => {
        it("hides manages software automations when all teams selected", () => {
          cy.getAttached(".manage-software-page__header-wrap").within(() => {
            cy.getAttached(".Select").within(() => {
              cy.findByText(/all teams/i).should("exist");
            });
            cy.getAttached(".manage-software-page__header-wrap").within(() => {
              cy.findByRole("button", {
                name: /manage automations/i,
              }).should("not.exist");
            });
          });
        });
        it("hides manage automations button when all teams not selected", () => {
          cy.getAttached(".manage-software-page__header-wrap").within(() => {
            cy.getAttached(".Select").within(() => {
              cy.getAttached(".Select-control").click();
              cy.getAttached(".Select-menu-outer").within(() => {
                cy.findByText(/apples/i).should("exist");
              });
              cy.findByRole("button", {
                name: /manage automations/i,
              }).should("not.exist");
            });
          });
        });
      it("allows global maintainer to click 'Manage automations' button", () => {
        cy.getAttached(".button-wrap")
          .findByRole("button", { name: /manage automations/i })
          .click();
        cy.findByRole("button", { name: /cancel/i }).click();
    it("hides manage automations button since all teams not selected", () => {
      cy.getAttached(".manage-software-page__header-wrap").within(() => {
        cy.findByText(/apples/i).should("exist");
      });
    it("hides manage automations button when all teams not selected", () => {
      cy.getAttached(".manage-policies-page__header-wrap").within(() => {
        cy.findByText(/apples/i).should("exist");
      });
      cy.findByText(/manage automations/i).should("not.exist");
    });
      cy.findByRole("button", { name: /add member/i }).click();
        // On maintaining team, should see the "Manage automations" button
        cy.findByText(/manage automations/i).should("exist");
import React, { useState } from "react";
      staleTime: 30000, // TODO: Discuss a reasonable staleTime given that counts are only updated infrequently?
  const { config, isGlobalAdmin, currentUser, isPremiumTier } = useContext(
    AppContext
  );
    currentUser ? setUserEditing(currentUser) : setUserEditing(undefined);
  }, [
    showCreateUserModal,
    currentUser,
    setShowCreateUserModal,
    setUserEditing,
    setShowAddMemberModal,
  ]);
        renderFlash("success", `Successfully removed ${userEditing?.name}`);
        .then(() =>
            `${newMembers.users.length} members successfully added to ${currentTeam?.name}.`
          )
        )
            renderFlash("success", `Successfully edited ${userName}.`);
                `Could not edit ${userName}. Please try again.`
                <Button
                  variant="brand"
                  className={`${noMembersClass}__create-button`}
                  onClick={toggleAddUserModal}
                >
                  Add member
                </Button>
          onActionButtonClick={toggleAddUserModal}
          actionButtonText={"Add member"}
          defaultGlobalRole={userEditing?.global_role}
    defaultGlobalRole !== null
            {canAddOrRemovePolicy &&
    setConfig,
# Any user can write self (besides role) and change their own password.
  action == [write, change_password][_]
# Any user can read other users.
allow {
  object.type == "user"
  not is_null(subject)
  action == read
}

# Admins can write all users + roles + change passwords.
  action == [write, write_role, change_password][_]
## Team admins can create or edit new users, but not change their password.
  action == [write, write_role][_]
# All users can read activities
  not is_null(subject)
# Global admins and maintainers can read/write all packs
  subject.global_role == [admin,maintainer][_]
# Team admins and maintainers can read global packs
  is_null(object.team_ids)
  team_role(subject, subject.teams[_].id) == [admin,maintainer][_]
# Team admins and maintainers can read/write their team packs
  team_role(subject, object.team_ids[_]) == [admin,maintainer][_]
  action == [read, write][_]
# All users can read software
  not is_null(subject)
  object.type == "software"
		// Admin can read/write all and create new
		// Regular users can read all users and write self (besides roles), but not create
		{user: test.UserMaintainer, object: user, action: read, allow: true},
		{user: test.UserNoRoles, object: user, action: read, allow: true},
		{user: test.UserObserver, object: user, action: read, allow: true},
		// Team Admin can create/write/write role of any team user, but not change password
		{user: teamAdmin, object: user, action: read, allow: true},
				TeamIDs: []uint{1},
		// Team observer cannot read packs of the team.
				TeamIDs: []uint{1},
			allow:  false,
				TeamIDs: []uint{1},
				TeamIDs: []uint{2},
				TeamIDs: []uint{2},
		// Team maintainers can read global packs.
			allow:  true,
		// Team admins can read global packs.
			allow:  true,
	// ActionChangePassword is the permission to change a user's password. While
	// the Write action allows setting the password on creation of a user, the
	// ChangePassword action is required to modify an existing user's password.
	ID          uint     `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description,omitempty"`
	Platform    string   `json:"platform,omitempty"`
	Disabled    bool     `json:"disabled"`
	Type        *string  `json:"type" db:"pack_type"`
	Labels      []Target `json:"labels"`
	LabelIDs    []uint   `json:"label_ids"`
	Hosts       []Target `json:"hosts"`
	HostIDs     []uint   `json:"host_ids"`
	Teams       []Target `json:"teams"`
	TeamIDs     []uint   `json:"team_ids"`
	// admin user
	activities, err := svc.ListActivities(test.UserContext(test.UserAdmin), fleet.ListOptions{})
	require.NoError(t, err)
	require.Len(t, activities, 2)
	// anyone can read activities
	activities, err = svc.ListActivities(test.UserContext(test.UserNoRoles), fleet.ListOptions{})
	require.NoError(t, err)
	require.Len(t, activities, 2)
	authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
		if authctx, ok := authz_ctx.FromContext(ctx); ok {
			authctx.SetChecked()
		}
		if authctx, ok := authz_ctx.FromContext(ctx); ok {
			authctx.SetChecked()
		}
	if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionRead); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionWrite); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{}, fleet.ActionWrite); err != nil {
			"global admin",
			&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
			false,
			false,
			"global maintainer",
			&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
			false,
			false,
			"global observer",
			&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
			true,
			true,
			"team admin",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
			true,
			false,
			"team maintainer",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
			true,
			false,
			"team observer",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
			true,
			true,
	authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
	alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken)
	if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) {
	if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) {
	if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) {

			"global admin",
			&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
			false,
			false,
			"global maintainer",
			&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
			false,
			false,
			"global observer",
			&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
			true,
			true,
			"team admin",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
			true,
			false,
			"team maintainer",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
			true,
			false,
			"team observer",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
			true,
			true,
	if err := svc.authz.Authorize(ctx, &fleet.Software{}, fleet.ActionRead); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Software{}, fleet.ActionRead); err != nil {
	user := &fleet.User{ID: 3, Email: "foo@bar.com", GlobalRole: ptr.String(fleet.RoleObserver)}
	if err := svc.authz.Authorize(ctx, &fleet.Pack{TeamIDs: []uint{teamID}}, fleet.ActionRead); err != nil {
	_ = resp
	if err := svc.authz.Authorize(ctx, &fleet.Pack{TeamIDs: []uint{teamID}}, fleet.ActionWrite); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{TeamIDs: []uint{teamID}}, fleet.ActionWrite); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{TeamIDs: []uint{teamID}}, fleet.ActionWrite); err != nil {
		return &fleet.Pack{ID: 999}, nil
			true,
	if err := svc.authz.Authorize(ctx, &fleet.User{}, fleet.ActionRead); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.User{ID: id}, fleet.ActionRead); err != nil {
		return nil, err
	return svc.ds.UserByID(ctx, id)
	if err := svc.authz.Authorize(ctx, &fleet.User{}, fleet.ActionRead); err != nil {
		return nil, err
	}

			return nil, ctxerr.New(ctx, "Cannot edit global role as a team member")
			return nil, ctxerr.New(ctx, "Cannot modify teams in that way")
	if err := svc.authz.Authorize(ctx, &fleet.User{ID: id}, fleet.ActionWrite); err != nil {

	// If the user is of the right global role, then they can modify the teams
	if currentUser.GlobalRole != nil && (*currentUser.GlobalRole == fleet.RoleAdmin || *currentUser.GlobalRole == fleet.RoleMaintainer) {
	// otherwise, gather the resulting teams
	resultingTeams := make(map[uint]string)
		resultingTeams[team.ID] = team.Role
	// and see which ones were removed or changed from the original
		if resultingTeams[team.ID] != team.Role {
	// then gather the teams the current user is admin for
	// and let's check that the teams that were either removed or changed are also teams this user is an admin of
		if id == 999 {
				ID:    999,
		return &fleet.User{
			ID:         888,
			GlobalRole: ptr.String(fleet.RoleMaintainer),
		}, nil
		name                  string
		user                  *fleet.User
		shouldFailRead        bool
		shouldFailDeleteReset bool
			"global admin",
			&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
			false,
			false,
			false,
			false,
			"global maintainer",
			&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
			true,
			true,
			false,
			true,
			"global observer",
			&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
			true,
			true,
			false,
			true,
			"team admin, belongs to team",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
			true,
			false,
			false,
			true,
			"team maintainer, belongs to team",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
			true,
			true,
			false,
			true,
			"team observer, belongs to team",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
			true,
			true,
			false,
			true,
			"team maintainer, DOES NOT belong to team",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
			true,
			true,
			false,
			true,
			"team admin, DOES NOT belong to team",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
			true,
			true,
			false,
			true,
			"team observer, DOES NOT belong to team",
			&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
			true,
			true,
			false,
			true,
			_, err := svc.CreateUser(ctx, fleet.UserPayload{
			_, err = svc.ModifyUser(ctx, 999, fleet.UserPayload{Teams: &teams})
			_, err = svc.ModifyUser(ctx, 888, fleet.UserPayload{Teams: &teams})
			checkAuthErr(t, tt.shouldFailGlobalWrite, err)
			_, err = svc.ModifyUser(ctx, 888, fleet.UserPayload{GlobalRole: ptr.String(fleet.RoleMaintainer)})
			checkAuthErr(t, tt.shouldFailGlobalWrite, err)
			err = svc.DeleteUser(ctx, 999)
			checkAuthErr(t, tt.shouldFailDeleteReset, err)
			_, err = svc.RequirePasswordReset(ctx, 999, false)
			checkAuthErr(t, tt.shouldFailDeleteReset, err)
			_, err = svc.ListUsers(ctx, fleet.UserListOptions{})
			checkAuthErr(t, tt.shouldFailRead, err)
			_, err = svc.User(ctx, 999)
			checkAuthErr(t, tt.shouldFailRead, err)
			_, err = svc.User(ctx, 888)
			checkAuthErr(t, tt.shouldFailRead, err)
Fixed code: * Restrict non-global user from access to activities
* Fix access control issues with "user" endpoints.
* Fix access control issues with "pack" endpoints.
* Fix access control issues with "software" endpoints.
        cy.findByText(/manage automations/).should("not.exist");
      it("hides manage automations from maintainer", () => {
        cy.getAttached(".button-wrap").within(() => {
          cy.findByRole("button", { name: /add a policy/i }).should("exist");
          cy.findByRole("button", { name: /manage automations/i }).should(
            "not.exist"
          );
        });
      cy.findByText(/manage automations/i).should("not.exist");
      cy.findByText(/add a policy/).should("not.exist");
    it("allows global admin to automate a team policy", () => {
      cy.visit("/policies/manage");
      cy.getAttached(".Select-control").within(() => {
        cy.findByText(/all teams/i).click();
      });
      cy.getAttached(".Select-menu")
        .contains(/apples/i)
        .click();
      cy.getAttached(".button-wrap")
        .findByRole("button", { name: /manage automations/i })
        .click();
      cy.getAttached(".manage-automations-modal").within(() => {
        cy.getAttached(".fleet-slider").click();
        cy.getAttached(".fleet-checkbox__input").check({ force: true });
        cy.getAttached("#webhook-url")
          .clear()
          .type("https://example.com/global_admin");
        cy.findByText(/save/i).click();
      });
      cy.findByText(/successfully updated policy automations/i).should("exist");
    });
      it("hides 'Manage automations' button from global maintainer", () => {
        cy.findByText(/manage automations/i).should("not.exist");
      it("hides manage automations button", () => {
        cy.findByText(/manage hosts/i).should("not.exist");
    it("hides manage automations button", () => {
      cy.findByRole("button", { name: /create user/i }).click();
        // On maintaining team, should not see the "Manage automations" button
        cy.findByText(/manage automations/i).should("not.exist");
import React, { useContext, useState } from "react";
import { AppContext } from "context/app";
  const { availableTeams, currentTeam, isOnGlobalTeam } = useContext(
    AppContext
  );

      enabled:
        isOnGlobalTeam ||
        !!availableTeams?.find((t) => t.id === currentTeam?.id),
      staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval
  const {
    config,
    currentUser,
    isGlobalAdmin,
    isPremiumTier,
    isTeamAdmin,
  } = useContext(AppContext);
  }, [showCreateUserModal, setShowCreateUserModal, setShowAddMemberModal]);
        renderFlash(
          "success",
          `Successfully removed ${userEditing?.name || "member"}`
        );
        .then(() => {
          const count = newMembers.users.length;
            `${count} ${
              count === 1 ? "member" : "members"
            } successfully added to ${currentTeam?.name}.`
          );
        })
            renderFlash(
              "success",
              `Successfully edited ${userName || "member"}.`
            );
                `Could not edit ${userName || "member"}. Please try again.`
                {isGlobalAdmin && (
                  <Button
                    variant="brand"
                    className={`${noMembersClass}__create-button`}
                    onClick={toggleAddUserModal}
                  >
                    Add member
                  </Button>
                )}
                {isTeamAdmin && (
                  <Button
                    variant="brand"
                    className={`${noMembersClass}__create-button`}
                    onClick={toggleCreateMemberModal}
                  >
                    Create user
                  </Button>
                )}
          onActionButtonClick={
            isGlobalAdmin ? toggleAddUserModal : toggleCreateMemberModal
          }
          actionButtonText={isGlobalAdmin ? "Add member" : "Create user"}
          defaultGlobalRole={userEditing?.global_role || null}
    !!defaultGlobalRole
  const canManageAutomations = isGlobalAdmin || isTeamAdmin;
            {canManageAutomations &&
    isOnGlobalTeam,
      enabled:
        isOnGlobalTeam ||
        !!availableTeams?.find((t) => t.id === currentTeam?.id),
      enabled:
        isOnGlobalTeam ||
        !!availableTeams?.find((t) => t.id === currentTeam?.id),
// ExtraAuthzer is the interface to implement extra fields for the policy.
type ExtraAuthzer interface {
	// ExtraAuthz returns the extra key/value pairs for the type.
	ExtraAuthz() (map[string]interface{}, error)
}

	// Add any extra key/values defined by the type.
	if extra, ok := in.(ExtraAuthzer); ok {
		extraKVs, err := extra.ExtraAuthz()
		if err != nil {
			return nil, fmt.Errorf("extra authz: %w", err)
		}
		for k, v := range extraKVs {
			if _, ok := out[k]; ok {
				return nil, fmt.Errorf("existing authz value: %s", k)
			}
			out[k] = v
		}
	}

# User specific actions

# Query specific actions
#
# NOTE: More rules apply to users but they are implemented in Go code.
# Our end goal is to move all the authorization logic here.
# Any user can read and write self and change their own password.
  action == [read, write, change_password][_]
# Global admins can perform all operations on all users.
  action == [read, write, write_role, change_password][_]
# Team admins can perform all operations on the team users (except changing their password).
  action == [read, write, write_role][_]
# Only global users can read activities
  not is_null(subject.global_role)
# Global admins and maintainers can read/write all packs.
  subject.global_role == [admin, maintainer][_]
# All users can read the global pack.
  not is_null(subject)
  object.is_global_pack == true
# Team admins, maintainers and observers can read their team's pack.
#
# NOTE: Action "read" on a team's pack includes listing its scheduled queries.
  not is_null(object.pack_team_id)
  team_role(subject, object.pack_team_id) == [admin, maintainer, observer][_]
  action == read
}

# Team admins and maintainers can add/remove scheduled queries from/to their team's pack.
#
# NOTE: The team's pack is not editable per-se, it's a special pack to group
# all the team's scheduled queries. So the "write" operation only covers
# adding/removing scheduled queries from the pack.
allow {
  object.type == "pack"
  not is_null(object.pack_team_id)
  team_role(subject, object.pack_team_id) == [admin, maintainer][_]
  action == write
# Global users can read all software.
  object.type == "software_inventory"
  subject.global_role == [admin, maintainer, observer][_]

# Team users can read all software in their teams.
allow {
  not is_null(object.team_id)
  object.type == "software_inventory"
  team_role(subject, object.team_id) == [admin, maintainer, observer][_]
  action == read
}
		// Global admin can read/write all and create new.
		// Global maintainers cannot read/write users.
		{user: test.UserMaintainer, object: user, action: read, allow: false},
		// Global maintainers cannot create users.
		// Global maintainers can read/write itself (besides roles).
		// Users without roles cannot read/write users.
		{user: test.UserNoRoles, object: user, action: read, allow: false},
		// User without roles cannot add new users.
		// User without roles can read/write itself (besides roles).
		// Global observers cannot read/write users.
		{user: test.UserObserver, object: user, action: read, allow: false},
		// Global observers cannot create users.
		// Global observers can read/write itself (besides roles).
		// Team admins cannot read/write global users.
		{user: teamAdmin, object: user, action: read, allow: false},
		// Team admins cannot create new global users.
		// Team admins can read/write team users (except change their password).
		// Team admins can add new users to the team.
		// Team admins can read/write itself.
				Type: ptr.String("team-1"),
		// Team observer can read packs of the team.
				Type: ptr.String("team-1"),
			allow:  true,
				Type: ptr.String("team-1"),
				Type: ptr.String("team-2"),
				Type: ptr.String("team-2"),
		// Team maintainers cannot read global packs.
			allow:  false,
		// Team admins cannot read global packs.
			allow:  false,

	//
	// User specific actions
	//

	//
	// While the Write action allows setting the role on creation of a user, the
	// ActionWriteRole action is required to modify an existing user's password.
	// ActionChangePassword is the permission to change a user's password.
	//
	// While the Write action allows setting the password on creation of a user, the
	// ActionChangePassword action is required to modify an existing user's password.

	//
	// Query specific actions
	//

	"strconv"
	"strings"

	"github.com/fleetdm/fleet/v4/server/ptr"
//
// NOTE: A "team pack" is a special type of pack with Pack.Type="team-$TEAM_ID".
// Such team packs hold the scheduled queries for a team. This is different from a
// pack that has a team as target (Pack.Teams and Pack.TeamIDs fields).
	ID          uint   `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Platform    string `json:"platform,omitempty"`
	Disabled    bool   `json:"disabled"`
	// Type indicates the type of the pack:
	//	- "global" is the type of the global pack.
	//	- "team-$ID" is the type for team packs.
	//	- nil is the type for a user created pack.
	Type     *string  `json:"type" db:"pack_type"`
	Labels   []Target `json:"labels"`
	LabelIDs []uint   `json:"label_ids"`
	Hosts    []Target `json:"hosts"`
	HostIDs  []uint   `json:"host_ids"`
	Teams    []Target `json:"teams"`
	// TeamIDs holds the ID of the teams this pack should target.
	TeamIDs []uint `json:"team_ids"`
}

// isTeamPack returns true if the pack is a pack specifically made for a team.
func (p *Pack) isTeamPack() bool {
	return p.Type != nil && strings.HasPrefix(*p.Type, "team-")
}

// isGlobalPack returns true if the pack is the global pack.
func (p *Pack) isGlobalPack() bool {
	return p.Type != nil && *p.Type == "global"
}

// TeamPack returns the team ID for a team's pack.
// Returns (nil, nil) if the pack is not a team pack.
func (p *Pack) teamPack() (*uint, error) {
	if !p.isTeamPack() {
		return nil, nil
	}
	t := strings.TrimPrefix(*p.Type, "team-")
	teamID, err := strconv.ParseUint(t, 10, 64)
	if err != nil {
		return nil, err
	}
	return ptr.Uint(uint(teamID)), nil
}

// ExtraAuthz implements authz.ExtraAuthzer.
func (p *Pack) ExtraAuthz() (map[string]interface{}, error) {
	packTeamID, err := p.teamPack()
	if err != nil {
		return nil, err
	}
	return map[string]interface{}{
		"pack_team_id":   packTeamID,
		"is_global_pack": p.isGlobalPack(),
	}, nil
// AuthzSoftwareInventory is used for access controls on software inventory.
type AuthzSoftwareInventory struct {
	// TeamID is the ID of the team. A value of nil means global scope.
	TeamID *uint `json:"team_id"`
}

// AuthzType implements authz.AuthzTyper.
func (s *AuthzSoftwareInventory) AuthzType() string {
	return "software_inventory"
}

	globalUsers := []*fleet.User{test.UserAdmin, test.UserMaintainer, test.UserObserver}
	teamUsers := []*fleet.User{test.UserTeamAdminTeam1, test.UserTeamMaintainerTeam1, test.UserTeamObserverTeam1}

	// any global user can read activities
	for _, u := range globalUsers {
		activities, err := svc.ListActivities(test.UserContext(u), fleet.ListOptions{})
		require.NoError(t, err)
		require.Len(t, activities, 2)
	}

	// team users cannot read activities
	for _, u := range teamUsers {
		_, err := svc.ListActivities(test.UserContext(u), fleet.ListOptions{})
		require.Error(t, err)
		require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
	}
	// user with no roles cannot read activities
	_, err := svc.ListActivities(test.UserContext(test.UserNoRoles), fleet.ListOptions{})
	require.Error(t, err)
	require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
		setAuthCheckedOnPreAuthErr(ctx)
		setAuthCheckedOnPreAuthErr(ctx)
	"fmt"
	if err := svc.authz.Authorize(ctx, &fleet.Pack{
		Type: ptr.String("global"),
	}, fleet.ActionRead); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{
		Type: ptr.String("global"),
	}, fleet.ActionRead); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{
		Type: ptr.String("global"),
	}, fleet.ActionWrite); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{
		Type: ptr.String("global"),
	}, fleet.ActionWrite); err != nil {
	globalPack, err := svc.ds.EnsureGlobalPack(ctx)
	if err != nil {
		return err
	}
	scheduledQuery, err := svc.ds.ScheduledQuery(ctx, id)
	if err != nil {
		return err
	}
	if scheduledQuery.PackID != globalPack.ID {
		return fmt.Errorf("scheduled query %d is not global", id)
	}

			name:            "global admin",
			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
			shouldFailWrite: false,
			shouldFailRead:  false,
			name:            "global maintainer",
			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
			shouldFailWrite: false,
			shouldFailRead:  false,
			name:            "global observer",
			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
			shouldFailWrite: true,
			shouldFailRead:  false,
			name:            "team admin",
			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
			shouldFailWrite: true,
			shouldFailRead:  false,
			name:            "team maintainer",
			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
			shouldFailWrite: true,
			shouldFailRead:  false,
			name:            "team observer",
			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
			shouldFailWrite: true,
			shouldFailRead:  false,
	alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken)
	if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
	if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
	if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
			name:            "global admin",
			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
			shouldFailWrite: false,
			shouldFailRead:  false,
			name:            "global maintainer",
			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
			shouldFailWrite: false,
			shouldFailRead:  false,
			name:            "global observer",
			user:            &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
			shouldFailWrite: true,
			shouldFailRead:  true,
		// Team users cannot read or write scheduled queries using the below service APIs.
		// Team users must use the "Team" endpoints (GetTeamScheduledQueries, TeamScheduleQuery,
		// ModifyTeamScheduledQueries and DeleteTeamScheduledQueries).
			name:            "team admin",
			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
			shouldFailWrite: true,
			shouldFailRead:  true,
			name:            "team maintainer",
			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
			shouldFailWrite: true,
			shouldFailRead:  true,
			name:            "team observer",
			user:            &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
			shouldFailWrite: true,
			shouldFailRead:  true,
	if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
		TeamID: opt.TeamID,
	}, fleet.ActionRead); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
		TeamID: opt.TeamID,
	}, fleet.ActionRead); err != nil {
	user := &fleet.User{
		ID:         3,
		Email:      "foo@bar.com",
		GlobalRole: ptr.String(fleet.RoleAdmin),
	}

func TestServiceSoftwareInventoryAuth(t *testing.T) {
	ds := new(mock.Store)

	ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) {
		return []fleet.Software{}, nil
	}
	ds.CountSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) {
		return 0, nil
	}
	svc := newTestService(t, ds, nil, nil)

	for _, tc := range []struct {
		name                 string
		user                 *fleet.User
		shouldFailGlobalRead bool
		shouldFailTeamRead   bool
	}{
		{
			name: "global-admin",
			user: &fleet.User{
				ID:         1,
				GlobalRole: ptr.String(fleet.RoleAdmin),
			},
			shouldFailGlobalRead: false,
			shouldFailTeamRead:   false,
		},
		{
			name: "global-maintainer",
			user: &fleet.User{
				ID:         1,
				GlobalRole: ptr.String(fleet.RoleMaintainer),
			},
			shouldFailGlobalRead: false,
			shouldFailTeamRead:   false,
		},
		{
			name: "global-observer",
			user: &fleet.User{
				ID:         1,
				GlobalRole: ptr.String(fleet.RoleObserver),
			},
			shouldFailGlobalRead: false,
			shouldFailTeamRead:   false,
		},
		{
			name: "team-admin-belongs-to-team",
			user: &fleet.User{
				ID: 1,
				Teams: []fleet.UserTeam{{
					Team: fleet.Team{ID: 1},
					Role: fleet.RoleAdmin,
				}},
			},
			shouldFailGlobalRead: true,
			shouldFailTeamRead:   false,
		},
		{
			name: "team-maintainer-belongs-to-team",
			user: &fleet.User{
				ID: 1,
				Teams: []fleet.UserTeam{{
					Team: fleet.Team{ID: 1},
					Role: fleet.RoleMaintainer,
				}},
			},
			shouldFailGlobalRead: true,
			shouldFailTeamRead:   false,
		},
		{
			name: "team-observer-belongs-to-team",
			user: &fleet.User{
				ID: 1,
				Teams: []fleet.UserTeam{{
					Team: fleet.Team{ID: 1},
					Role: fleet.RoleObserver,
				}},
			},
			shouldFailGlobalRead: true,
			shouldFailTeamRead:   false,
		},
		{
			name: "team-admin-does-not-belong-to-team",
			user: &fleet.User{
				ID: 1,
				Teams: []fleet.UserTeam{{
					Team: fleet.Team{ID: 2},
					Role: fleet.RoleAdmin,
				}},
			},
			shouldFailGlobalRead: true,
			shouldFailTeamRead:   true,
		},
		{
			name: "team-maintainer-does-not-belong-to-team",
			user: &fleet.User{
				ID: 1,
				Teams: []fleet.UserTeam{{
					Team: fleet.Team{ID: 2},
					Role: fleet.RoleMaintainer,
				}},
			},
			shouldFailGlobalRead: true,
			shouldFailTeamRead:   true,
		},
		{
			name: "team-observer-does-not-belong-to-team",
			user: &fleet.User{
				ID: 1,
				Teams: []fleet.UserTeam{{
					Team: fleet.Team{ID: 2},
					Role: fleet.RoleObserver,
				}},
			},
			shouldFailGlobalRead: true,
			shouldFailTeamRead:   true,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tc.user})

			// List all software.
			_, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{})
			checkAuthErr(t, tc.shouldFailGlobalRead, err)

			// Count all software.
			_, err = svc.CountSoftware(ctx, fleet.SoftwareListOptions{})
			checkAuthErr(t, tc.shouldFailGlobalRead, err)

			// List software for a team.
			_, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{
				TeamID: ptr.Uint(1),
			})
			checkAuthErr(t, tc.shouldFailTeamRead, err)

			// Count software for a team.
			_, err = svc.CountSoftware(ctx, fleet.SoftwareListOptions{
				TeamID: ptr.Uint(1),
			})
			checkAuthErr(t, tc.shouldFailTeamRead, err)
		})
	}
}
	"fmt"
	if err := svc.authz.Authorize(ctx, &fleet.Pack{
		Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
	}, fleet.ActionRead); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{
		Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
	}, fleet.ActionWrite); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{
		Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
	}, fleet.ActionWrite); err != nil {
	if err := svc.authz.Authorize(ctx, &fleet.Pack{
		Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
	}, fleet.ActionWrite); err != nil {
	"fmt"
		return &fleet.Pack{
			ID:   999,
			Type: ptr.String(fmt.Sprintf("team-%d", teamID)),
		}, nil
			false,
	authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
	user := &fleet.User{}
	if opt.TeamID != 0 {
		user.Teams = []fleet.UserTeam{{Team: fleet.Team{ID: opt.TeamID}}}
	}
	if err := svc.authz.Authorize(ctx, user, fleet.ActionRead); err != nil {
// setAuthCheckedOnPreAuthErr can be used to set the authentication as checked
// in case of errors that happened before an auth check can be performed.
// Otherwise the endpoints return a "authentication skipped" error instead of
// the actual returned error.
func setAuthCheckedOnPreAuthErr(ctx context.Context) {
	if az, ok := authz_ctx.FromContext(ctx); ok {
		az.SetChecked()
	}
}

	user, err := svc.ds.UserByID(ctx, id)
	if err != nil {
		setAuthCheckedOnPreAuthErr(ctx)
		return nil, ctxerr.Wrap(ctx, err)
	if err := svc.authz.Authorize(ctx, user, fleet.ActionRead); err != nil {
		return nil, err
	}
	return user, nil
		setAuthCheckedOnPreAuthErr(ctx)
			return nil, authz.ForbiddenWithInternal(
				"cannot edit global role as a team member",
				currentUser, user, fleet.ActionWriteRole,
			)
			return nil, authz.ForbiddenWithInternal(
				"cannot modify teams in that way",
				currentUser, user, fleet.ActionWriteRole,
			)
	user, err := svc.ds.UserByID(ctx, id)
	if err != nil {
		setAuthCheckedOnPreAuthErr(ctx)
		return ctxerr.Wrap(ctx, err)
	}
	if err := svc.authz.Authorize(ctx, user, fleet.ActionWrite); err != nil {
// isAdminOfTheModifiedTeams checks whether the current user is allowed to modify the user
// roles in the teams.
//
// TODO: End-goal is to move all this logic to policy.rego.
	// Global admins can modify all user teams roles.
	if currentUser.GlobalRole != nil && *currentUser.GlobalRole == fleet.RoleAdmin {
	// Otherwise, make a map of the original and resulting teams.
	newTeams := make(map[uint]string)
		newTeams[team.ID] = team.Role
	}
	originalTeams := make(map[uint]struct{})
	for _, team := range originalUserTeams {
		originalTeams[team.ID] = struct{}{}
	// See which ones were removed or changed from the original.
		if newTeams[team.ID] != team.Role {
			teamsAffected[team.ID] = struct{}{}
		}
	}

	// See which ones of the new are not in the original.
	for _, team := range newUserTeams {
		if _, ok := originalTeams[team.ID]; !ok {
	// Then gather the teams the current user is admin for.
	// And finally, let's check that the teams that were either removed
	// or changed are also teams this user is an admin of.
	"github.com/fleetdm/fleet/v4/server/authz"

	userTeamMaintainerID := uint(999)
	userGlobalMaintainerID := uint(888)
	var self *fleet.User // to be set by tests
		switch id {
		case userTeamMaintainerID:
				ID:    userTeamMaintainerID,
		case userGlobalMaintainerID:
			return &fleet.User{
				ID:         userGlobalMaintainerID,
				GlobalRole: ptr.String(fleet.RoleMaintainer),
			}, nil
		default:
			return self, nil

		name string
		user *fleet.User


		shouldFailWriteRoleGlobalToGlobal    bool
		shouldFailWriteRoleGlobalToTeam      bool
		shouldFailWriteRoleTeamToAnotherTeam bool
		shouldFailWriteRoleTeamToGlobal      bool

		shouldFailWriteRoleOwnDomain bool

		shouldFailGlobalRead bool
		shouldFailTeamRead   bool

		shouldFailGlobalDelete bool
		shouldFailTeamDelete   bool

		shouldFailGlobalPasswordReset bool
		shouldFailTeamPasswordReset   bool

		shouldFailGlobalChangePassword bool
		shouldFailTeamChangePassword   bool

		shouldFailListAll  bool
		shouldFailListTeam bool
			name:                                 "global admin",
			user:                                 &fleet.User{ID: 1000, GlobalRole: ptr.String(fleet.RoleAdmin)},
			shouldFailGlobalWrite:                false,
			shouldFailTeamWrite:                  false,
			shouldFailWriteRoleGlobalToGlobal:    false,
			shouldFailWriteRoleGlobalToTeam:      false,
			shouldFailWriteRoleTeamToAnotherTeam: false,
			shouldFailWriteRoleTeamToGlobal:      false,
			shouldFailWriteRoleOwnDomain:         false,
			shouldFailGlobalRead:                 false,
			shouldFailTeamRead:                   false,
			shouldFailGlobalDelete:               false,
			shouldFailTeamDelete:                 false,
			shouldFailGlobalPasswordReset:        false,
			shouldFailTeamPasswordReset:          false,
			shouldFailGlobalChangePassword:       false,
			shouldFailTeamChangePassword:         false,
			shouldFailListAll:                    false,
			shouldFailListTeam:                   false,
			name:                                 "global maintainer",
			user:                                 &fleet.User{ID: 1000, GlobalRole: ptr.String(fleet.RoleMaintainer)},
			shouldFailGlobalWrite:                true,
			shouldFailTeamWrite:                  true,
			shouldFailWriteRoleGlobalToGlobal:    true,
			shouldFailWriteRoleGlobalToTeam:      true,
			shouldFailWriteRoleTeamToAnotherTeam: true,
			shouldFailWriteRoleTeamToGlobal:      true,
			shouldFailWriteRoleOwnDomain:         true,
			shouldFailGlobalRead:                 true,
			shouldFailTeamRead:                   true,
			shouldFailGlobalDelete:               true,
			shouldFailTeamDelete:                 true,
			shouldFailGlobalPasswordReset:        true,
			shouldFailTeamPasswordReset:          true,
			shouldFailGlobalChangePassword:       true,
			shouldFailTeamChangePassword:         true,
			shouldFailListAll:                    true,
			shouldFailListTeam:                   true,
			name:                                 "global observer",
			user:                                 &fleet.User{ID: 1000, GlobalRole: ptr.String(fleet.RoleObserver)},
			shouldFailGlobalWrite:                true,
			shouldFailTeamWrite:                  true,
			shouldFailWriteRoleGlobalToGlobal:    true,
			shouldFailWriteRoleGlobalToTeam:      true,
			shouldFailWriteRoleTeamToAnotherTeam: true,
			shouldFailWriteRoleTeamToGlobal:      true,
			shouldFailWriteRoleOwnDomain:         true,
			shouldFailGlobalRead:                 true,
			shouldFailTeamRead:                   true,
			shouldFailGlobalDelete:               true,
			shouldFailTeamDelete:                 true,
			shouldFailGlobalPasswordReset:        true,
			shouldFailTeamPasswordReset:          true,
			shouldFailGlobalChangePassword:       true,
			shouldFailTeamChangePassword:         true,
			shouldFailListAll:                    true,
			shouldFailListTeam:                   true,
			name:                                 "team admin, belongs to team",
			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
			shouldFailGlobalWrite:                true,
			shouldFailTeamWrite:                  false,
			shouldFailWriteRoleGlobalToGlobal:    true,
			shouldFailWriteRoleGlobalToTeam:      true,
			shouldFailWriteRoleTeamToAnotherTeam: true,
			shouldFailWriteRoleTeamToGlobal:      true,
			shouldFailWriteRoleOwnDomain:         false,
			shouldFailGlobalRead:                 true,
			shouldFailTeamRead:                   false,
			shouldFailGlobalDelete:               true,
			shouldFailTeamDelete:                 false,
			shouldFailGlobalPasswordReset:        true,
			shouldFailTeamPasswordReset:          true,
			shouldFailGlobalChangePassword:       true,
			shouldFailTeamChangePassword:         true,
			shouldFailListAll:                    true,
			shouldFailListTeam:                   false,
			name:                                 "team maintainer, belongs to team",
			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
			shouldFailGlobalWrite:                true,
			shouldFailTeamWrite:                  true,
			shouldFailWriteRoleGlobalToGlobal:    true,
			shouldFailWriteRoleGlobalToTeam:      true,
			shouldFailWriteRoleTeamToAnotherTeam: true,
			shouldFailWriteRoleTeamToGlobal:      true,
			shouldFailWriteRoleOwnDomain:         true,
			shouldFailGlobalRead:                 true,
			shouldFailTeamRead:                   true,
			shouldFailGlobalDelete:               true,
			shouldFailTeamDelete:                 true,
			shouldFailGlobalPasswordReset:        true,
			shouldFailTeamPasswordReset:          true,
			shouldFailGlobalChangePassword:       true,
			shouldFailTeamChangePassword:         true,
			shouldFailListAll:                    true,
			shouldFailListTeam:                   true,
			name:                                 "team observer, belongs to team",
			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
			shouldFailGlobalWrite:                true,
			shouldFailTeamWrite:                  true,
			shouldFailWriteRoleGlobalToGlobal:    true,
			shouldFailWriteRoleGlobalToTeam:      true,
			shouldFailWriteRoleTeamToAnotherTeam: true,
			shouldFailWriteRoleTeamToGlobal:      true,
			shouldFailWriteRoleOwnDomain:         true,
			shouldFailGlobalRead:                 true,
			shouldFailTeamRead:                   true,
			shouldFailGlobalDelete:               true,
			shouldFailTeamDelete:                 true,
			shouldFailGlobalPasswordReset:        true,
			shouldFailTeamPasswordReset:          true,
			shouldFailGlobalChangePassword:       true,
			shouldFailTeamChangePassword:         true,
			shouldFailListAll:                    true,
			shouldFailListTeam:                   true,
			name:                                 "team maintainer, DOES NOT belong to team",
			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
			shouldFailGlobalWrite:                true,
			shouldFailTeamWrite:                  true,
			shouldFailWriteRoleGlobalToGlobal:    true,
			shouldFailWriteRoleGlobalToTeam:      true,
			shouldFailWriteRoleTeamToAnotherTeam: true,
			shouldFailWriteRoleTeamToGlobal:      true,
			shouldFailWriteRoleOwnDomain:         true,
			shouldFailGlobalRead:                 true,
			shouldFailTeamRead:                   true,
			shouldFailGlobalDelete:               true,
			shouldFailTeamDelete:                 true,
			shouldFailGlobalPasswordReset:        true,
			shouldFailTeamPasswordReset:          true,
			shouldFailGlobalChangePassword:       true,
			shouldFailTeamChangePassword:         true,
			shouldFailListAll:                    true,
			shouldFailListTeam:                   true,
			name:                                 "team admin, DOES NOT belong to team",
			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
			shouldFailGlobalWrite:                true,
			shouldFailTeamWrite:                  true,
			shouldFailWriteRoleGlobalToGlobal:    true,
			shouldFailWriteRoleGlobalToTeam:      true,
			shouldFailWriteRoleTeamToAnotherTeam: true,
			shouldFailWriteRoleTeamToGlobal:      true,
			shouldFailWriteRoleOwnDomain:         false, // this is testing changing its own role in the team it belongs to.
			shouldFailGlobalRead:                 true,
			shouldFailTeamRead:                   true,
			shouldFailGlobalDelete:               true,
			shouldFailTeamDelete:                 true,
			shouldFailGlobalPasswordReset:        true,
			shouldFailTeamPasswordReset:          true,
			shouldFailGlobalChangePassword:       true,
			shouldFailTeamChangePassword:         true,
			shouldFailListAll:                    true,
			shouldFailListTeam:                   true,
			name:                                 "team observer, DOES NOT belong to team",
			user:                                 &fleet.User{ID: 1000, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
			shouldFailGlobalWrite:                true,
			shouldFailTeamWrite:                  true,
			shouldFailWriteRoleGlobalToGlobal:    true,
			shouldFailWriteRoleGlobalToTeam:      true,
			shouldFailWriteRoleTeamToAnotherTeam: true,
			shouldFailWriteRoleTeamToGlobal:      true,
			shouldFailWriteRoleOwnDomain:         true,
			shouldFailGlobalRead:                 true,
			shouldFailTeamRead:                   true,
			shouldFailGlobalDelete:               true,
			shouldFailTeamDelete:                 true,
			shouldFailGlobalPasswordReset:        true,
			shouldFailTeamPasswordReset:          true,
			shouldFailGlobalChangePassword:       true,
			shouldFailTeamChangePassword:         true,
			shouldFailListAll:                    true,
			shouldFailListTeam:                   true,
			tt.user.SetPassword("p4ssw0rd.", 10, 10)

			// To test a user reading/modifying itself.
			u := *tt.user
			self = &u

			// A user can always read itself (read rego action).
			_, err := svc.User(ctx, tt.user.ID)
			require.NoError(t, err)

			// A user can always write itself (write rego action).
			_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{Name: ptr.String("Foo")})
			require.NoError(t, err)

			// A user can always change its own password (change_password rego action).
			_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{Password: ptr.String("p4ssw0rd."), NewPassword: ptr.String("p4ssw0rd.3")})
			require.NoError(t, err)

			changeRole := func(role string) string {
				switch role {
				case fleet.RoleMaintainer:
					return fleet.RoleAdmin // promote
				case fleet.RoleAdmin:
					return fleet.RoleMaintainer // demote
				case fleet.RoleObserver:
					return fleet.RoleAdmin // promote
				default:
					t.Fatalf("unknown role: %s", role)
					return ""
				}
			}

			// Test a user modifying its own role within its domain (write_role rego action).
			if tt.user.GlobalRole != nil {
				_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{GlobalRole: ptr.String(changeRole(*tt.user.GlobalRole))})
				checkAuthErr(t, tt.shouldFailWriteRoleOwnDomain, err)
			} else { // Team user
				ownTeamDifferentRole := []fleet.UserTeam{
					{
						Team: fleet.Team{ID: tt.user.Teams[0].ID},
						Role: changeRole(tt.user.Teams[0].Role),
					},
				}
				_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{Teams: &ownTeamDifferentRole})
				checkAuthErr(t, tt.shouldFailWriteRoleOwnDomain, err)
			}

			_, err = svc.CreateUser(ctx, fleet.UserPayload{
			_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{Name: ptr.String("Foo")})
			checkAuthErr(t, tt.shouldFailGlobalWrite, err)

			_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{Name: ptr.String("Bar")})
			_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{GlobalRole: ptr.String(fleet.RoleMaintainer)})
			checkAuthErr(t, tt.shouldFailWriteRoleGlobalToGlobal, err)
			_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{Teams: &teams})
			checkAuthErr(t, tt.shouldFailWriteRoleGlobalToTeam, err)
			anotherTeams := []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}
			_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{Teams: &anotherTeams})
			checkAuthErr(t, tt.shouldFailWriteRoleTeamToAnotherTeam, err)
			_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{GlobalRole: ptr.String(fleet.RoleMaintainer)})
			checkAuthErr(t, tt.shouldFailWriteRoleTeamToGlobal, err)
			_, err = svc.User(ctx, userGlobalMaintainerID)
			checkAuthErr(t, tt.shouldFailGlobalRead, err)

			_, err = svc.User(ctx, userTeamMaintainerID)
			checkAuthErr(t, tt.shouldFailTeamRead, err)

			err = svc.DeleteUser(ctx, userGlobalMaintainerID)
			checkAuthErr(t, tt.shouldFailGlobalDelete, err)

			err = svc.DeleteUser(ctx, userTeamMaintainerID)
			checkAuthErr(t, tt.shouldFailTeamDelete, err)

			_, err = svc.RequirePasswordReset(ctx, userGlobalMaintainerID, false)
			checkAuthErr(t, tt.shouldFailGlobalPasswordReset, err)
			_, err = svc.RequirePasswordReset(ctx, userTeamMaintainerID, false)
			checkAuthErr(t, tt.shouldFailTeamPasswordReset, err)
			_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{NewPassword: ptr.String("passw0rd.2")})
			checkAuthErr(t, tt.shouldFailGlobalChangePassword, err)

			_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{NewPassword: ptr.String("passw0rd.2")})
			checkAuthErr(t, tt.shouldFailTeamChangePassword, err)

			_, err = svc.ListUsers(ctx, fleet.UserListOptions{})
			checkAuthErr(t, tt.shouldFailListAll, err)

			_, err = svc.ListUsers(ctx, fleet.UserListOptions{TeamID: 1})
			checkAuthErr(t, tt.shouldFailListTeam, err)

func TestIsAdminOfTheModifiedTeams(t *testing.T) {
	type teamWithRole struct {
		teamID uint
		role   string
	}
	type roles struct {
		global *string
		teams  []teamWithRole
	}
	for _, tc := range []struct {
		name string
		// actionUserRoles are the roles of the user executing the role change action.
		actionUserRoles roles
		// targetUserOriginalTeams are the original teams the target user belongs to.
		targetUserOriginalTeams []teamWithRole
		// targetUserNewTeams are the new teams the target user will be added to.
		targetUserNewTeams []teamWithRole

		expected bool
	}{
		{
			name: "global-admin-allmighty",
			actionUserRoles: roles{
				global: ptr.String(fleet.RoleAdmin),
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 2,
					role:   fleet.RoleAdmin,
				},
			},
			expected: true,
		},
		{
			name: "global-maintainer-cannot-modify-team-users",
			actionUserRoles: roles{
				global: ptr.String(fleet.RoleMaintainer),
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleMaintainer,
				},
			},
			expected: false,
		},
		{
			name: "team-admin-of-original-and-new",
			actionUserRoles: roles{
				teams: []teamWithRole{
					{
						teamID: 1,
						role:   fleet.RoleAdmin,
					},
					{
						teamID: 2,
						role:   fleet.RoleAdmin,
					},
				},
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 2,
					role:   fleet.RoleAdmin,
				},
			},
			expected: true,
		},
		{
			name: "team-admin-of-one-original-and-leave-other-team-unmodified",
			actionUserRoles: roles{
				teams: []teamWithRole{
					{
						teamID: 1,
						role:   fleet.RoleMaintainer,
					},
					{
						teamID: 2,
						role:   fleet.RoleAdmin,
					},
				},
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleMaintainer,
				},
				{
					teamID: 2,
					role:   fleet.RoleMaintainer,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleMaintainer,
				},
				{
					teamID: 2,
					role:   fleet.RoleAdmin,
				},
			},
			expected: true,
		},
		{
			name: "team-admin-of-original-only",
			actionUserRoles: roles{
				teams: []teamWithRole{
					{
						teamID: 1,
						role:   fleet.RoleAdmin,
					},
					{
						teamID: 2,
						role:   fleet.RoleMaintainer,
					},
				},
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 2,
					role:   fleet.RoleAdmin,
				},
			},
			expected: false,
		},
		{
			name: "team-admin-of-new-only",
			actionUserRoles: roles{
				teams: []teamWithRole{
					{
						teamID: 1,
						role:   fleet.RoleObserver,
					},
					{
						teamID: 2,
						role:   fleet.RoleAdmin,
					},
				},
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 2,
					role:   fleet.RoleAdmin,
				},
			},
			expected: false,
		},
		{
			name: "team-admin-but-new-another-team-observer",
			actionUserRoles: roles{
				teams: []teamWithRole{
					{
						teamID: 1,
						role:   fleet.RoleAdmin,
					},
				},
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
				{
					teamID: 2,
					role:   fleet.RoleObserver,
				},
			},
			expected: false,
		},
		{
			name: "team-admin-but-new-another-team-admin",
			actionUserRoles: roles{
				teams: []teamWithRole{
					{
						teamID: 1,
						role:   fleet.RoleAdmin,
					},
				},
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
				{
					teamID: 2,
					role:   fleet.RoleAdmin,
				},
			},
			expected: false,
		},
		{
			name: "team-admin-but-original-another-team",
			actionUserRoles: roles{
				teams: []teamWithRole{
					{
						teamID: 1,
						role:   fleet.RoleAdmin,
					},
				},
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 2,
					role:   fleet.RoleAdmin,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
			},
			expected: false,
		},
		{
			name: "team-admin-but-change-role-another-team",
			actionUserRoles: roles{
				teams: []teamWithRole{
					{
						teamID: 1,
						role:   fleet.RoleAdmin,
					},
				},
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
				{
					teamID: 2,
					role:   fleet.RoleAdmin,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
				{
					teamID: 2,
					role:   fleet.RoleMaintainer,
				},
			},
			expected: false,
		},
		{
			name: "team-admin-of-one-original-only",
			actionUserRoles: roles{
				teams: []teamWithRole{
					{
						teamID: 1,
						role:   fleet.RoleMaintainer,
					},
					{
						teamID: 2,
						role:   fleet.RoleAdmin,
					},
				},
			},
			targetUserOriginalTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleMaintainer,
				},
				{
					teamID: 2,
					role:   fleet.RoleMaintainer,
				},
			},
			targetUserNewTeams: []teamWithRole{
				{
					teamID: 1,
					role:   fleet.RoleAdmin,
				},
				{
					teamID: 2,
					role:   fleet.RoleAdmin,
				},
			},
			expected: false,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			userTeamsFn := func(twr []teamWithRole) []fleet.UserTeam {
				var userTeams []fleet.UserTeam
				for _, ot := range twr {
					userTeams = append(userTeams, fleet.UserTeam{
						Team: fleet.Team{ID: ot.teamID},
						Role: ot.role,
					})
				}
				return userTeams
			}

			actionUserTeams := userTeamsFn(tc.actionUserRoles.teams)
			originalUserTeams := userTeamsFn(tc.targetUserOriginalTeams)
			newUserTeams := userTeamsFn(tc.targetUserNewTeams)

			result := isAdminOfTheModifiedTeams(
				&fleet.User{
					GlobalRole: tc.actionUserRoles.global,
					Teams:      actionUserTeams,
				},
				originalUserTeams,
				newUserTeams,
			)
			require.Equal(t, tc.expected, result)
		})
	}
}

// TestAdminAddRoleOtherTeam is an explicit test to check that
// that an admin cannot add itself to another team.
func TestTeamAdminAddRoleOtherTeam(t *testing.T) {
	ds := new(mock.Store)
	svc := newTestService(t, ds, nil, nil)

	// adminTeam2 is a team admin of team with ID=2.
	adminTeam2 := &fleet.User{
		ID: 1,
		Teams: []fleet.UserTeam{
			{
				Team: fleet.Team{ID: 2},
				Role: fleet.RoleAdmin,
			},
		},
	}

	ds.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
		if id != 1 {
			return nil, &notFoundError{}
		}
		return adminTeam2, nil
	}
	ds.SaveUserFunc = func(ctx context.Context, user *fleet.User) error {
		return nil
	}

	ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: adminTeam2})
	adminTeam2.SetPassword("p4ssw0rd.", 10, 10)

	// adminTeam2 tries to add itself to team with ID=3 as admin.
	_, err := svc.ModifyUser(ctx, adminTeam2.ID, fleet.UserPayload{
		Teams: &[]fleet.UserTeam{
			{
				Team: fleet.Team{ID: 2},
				Role: fleet.RoleAdmin,
			},
			{
				Team: fleet.Team{ID: 3},
				Role: fleet.RoleAdmin,
			},
		},
	})
	require.Equal(t, (&authz.Forbidden{}).Error(), err.Error())
	require.False(t, ds.SaveUserFuncInvoked)
}