CVE-2022-24841 (Go, published 2022-04-18T22:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 8.1)
Weakness: CWE-863 \u2014 Incorrect Authorization
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.
Repository: https://github.com/fleetdm/fleet
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 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>

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)
}