Software title details now return installer scripts and managed app configuration only to users who can read the installer. Uninstalling software from the My device page now applies the same self-service and label scope rules as installing. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Security & Permissions** * Restricted installer scripts, managed-app settings, and related configuration to authorized viewers. * Preserved package metadata while hiding sensitive installation details from unauthorized roles. * Improved access handling for requests without an assigned team or involving inaccessible fleets. * **Bug Fixes** * Updated device-initiated software removal to honor self-service eligibility and label scope. * Added clearer errors when software is unavailable for self-service or outside the device’s scope. * Prevented software titles from inaccessible fleets from appearing in results. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
543 lines
17 KiB
Go
543 lines
17 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestServiceSoftwareTitlesAuth(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
|
|
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmf fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
|
|
return []fleet.SoftwareTitleListResult{}, 0, &fleet.PaginationMetadata{}, nil
|
|
}
|
|
ds.SoftwareTitleByIDFunc = func(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
|
|
return &fleet.SoftwareTitle{}, nil
|
|
}
|
|
ds.TeamExistsFunc = func(ctx context.Context, teamID uint) (bool, error) { return true, nil }
|
|
ds.SoftwareTitleByIDFunc = func(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
|
|
return &fleet.SoftwareTitle{BundleIdentifier: ptr.String("foo")}, nil
|
|
}
|
|
ds.UpdateSoftwareTitleNameFunc = func(ctx context.Context, id uint, name string) error {
|
|
return nil
|
|
}
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
user *fleet.User
|
|
shouldFailGlobalRead bool
|
|
shouldFailTeamRead bool
|
|
shouldFailGetByID bool
|
|
shouldFailWrite bool
|
|
}{
|
|
{
|
|
name: "global-admin",
|
|
user: &fleet.User{
|
|
ID: 1,
|
|
GlobalRole: ptr.String(fleet.RoleAdmin),
|
|
},
|
|
shouldFailGlobalRead: false,
|
|
shouldFailTeamRead: false,
|
|
shouldFailGetByID: false,
|
|
shouldFailWrite: false,
|
|
},
|
|
{
|
|
name: "global-maintainer",
|
|
user: &fleet.User{
|
|
ID: 1,
|
|
GlobalRole: ptr.String(fleet.RoleMaintainer),
|
|
},
|
|
shouldFailGlobalRead: false,
|
|
shouldFailTeamRead: false,
|
|
shouldFailGetByID: false,
|
|
shouldFailWrite: true,
|
|
},
|
|
{
|
|
name: "global-observer",
|
|
user: &fleet.User{
|
|
ID: 1,
|
|
GlobalRole: ptr.String(fleet.RoleObserver),
|
|
},
|
|
shouldFailGlobalRead: false,
|
|
shouldFailTeamRead: false,
|
|
shouldFailGetByID: false,
|
|
shouldFailWrite: true,
|
|
},
|
|
{
|
|
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,
|
|
shouldFailGetByID: false,
|
|
shouldFailWrite: true,
|
|
},
|
|
{
|
|
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,
|
|
shouldFailGetByID: false,
|
|
shouldFailWrite: true,
|
|
},
|
|
{
|
|
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,
|
|
shouldFailGetByID: false,
|
|
shouldFailWrite: true,
|
|
},
|
|
{
|
|
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,
|
|
shouldFailGetByID: true,
|
|
shouldFailWrite: 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,
|
|
shouldFailGetByID: true,
|
|
shouldFailWrite: 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,
|
|
shouldFailGetByID: true,
|
|
shouldFailWrite: true,
|
|
},
|
|
{
|
|
// GitOps can list software titles but cannot fetch a single title
|
|
// because SoftwareTitleByID also requires Host:list permission.
|
|
name: "global-gitops",
|
|
user: &fleet.User{
|
|
ID: 1,
|
|
GlobalRole: ptr.String(fleet.RoleGitOps),
|
|
},
|
|
shouldFailGlobalRead: false,
|
|
shouldFailTeamRead: false,
|
|
shouldFailGetByID: true,
|
|
shouldFailWrite: true,
|
|
},
|
|
{
|
|
name: "team-gitops-belongs-to-team",
|
|
user: &fleet.User{
|
|
ID: 1,
|
|
Teams: []fleet.UserTeam{{
|
|
Team: fleet.Team{ID: 1},
|
|
Role: fleet.RoleGitOps,
|
|
}},
|
|
},
|
|
shouldFailGlobalRead: true,
|
|
shouldFailTeamRead: false,
|
|
shouldFailGetByID: true,
|
|
shouldFailWrite: true,
|
|
},
|
|
{
|
|
name: "team-gitops-does-not-belong-to-team",
|
|
user: &fleet.User{
|
|
ID: 1,
|
|
Teams: []fleet.UserTeam{{
|
|
Team: fleet.Team{ID: 2},
|
|
Role: fleet.RoleGitOps,
|
|
}},
|
|
},
|
|
shouldFailGlobalRead: true,
|
|
shouldFailTeamRead: true,
|
|
shouldFailGetByID: true,
|
|
shouldFailWrite: true,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tc.user})
|
|
premiumCtx := license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
// List all software titles.
|
|
_, _, _, err := svc.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{})
|
|
checkAuthErr(t, tc.shouldFailGlobalRead, err)
|
|
|
|
// List software for a team.
|
|
_, _, _, err = svc.ListSoftwareTitles(premiumCtx, fleet.SoftwareTitleListOptions{
|
|
TeamID: ptr.Uint(1),
|
|
})
|
|
checkAuthErr(t, tc.shouldFailTeamRead, err)
|
|
|
|
// List software for a team should fail no matter what
|
|
// with a non-premium context
|
|
if !tc.shouldFailTeamRead {
|
|
_, _, _, err = svc.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
|
|
TeamID: ptr.Uint(1),
|
|
})
|
|
require.ErrorContains(t, err, "Requires Fleet Premium license")
|
|
}
|
|
|
|
// Get a software title for a team
|
|
_, err = svc.SoftwareTitleByID(ctx, 1, ptr.Uint(1))
|
|
checkAuthErr(t, tc.shouldFailGetByID, err)
|
|
|
|
// Update a software title's name
|
|
err = svc.UpdateSoftwareName(ctx, 1, "2 Chrome 2 Furious")
|
|
checkAuthErr(t, tc.shouldFailWrite, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSoftwareTitleByIDInstallerDetails covers the two authorization rules the
|
|
// title detail response depends on: the "no team" scope is authorized even when
|
|
// no fleet is given, and script contents and managed app configuration reach
|
|
// only callers who can read the installer, not merely software inventory.
|
|
func TestSoftwareTitleByIDInstallerDetails(t *testing.T) {
|
|
const (
|
|
installScript = "echo install"
|
|
uninstallScript = "echo uninstall"
|
|
postInstallScript = "echo post-install"
|
|
preInstallQuery = "SELECT 1;"
|
|
)
|
|
appConfiguration := json.RawMessage(`{"key":"value"}`)
|
|
|
|
ds := new(mock.Store)
|
|
ds.TeamExistsFunc = func(ctx context.Context, teamID uint) (bool, error) { return true, nil }
|
|
ds.SoftwareTitleByIDFunc = func(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
|
|
return &fleet.SoftwareTitle{
|
|
ID: id,
|
|
Name: "Foo",
|
|
SoftwareInstallersCount: 1,
|
|
VPPAppsCount: 1,
|
|
}, nil
|
|
}
|
|
ds.GetSoftwarePackagesByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint) ([]*fleet.SoftwareInstaller, error) {
|
|
return []*fleet.SoftwareInstaller{{
|
|
InstallerID: 1,
|
|
TitleID: &titleID,
|
|
Name: "foo.pkg",
|
|
Version: "1.0",
|
|
Platform: "darwin",
|
|
StorageID: "abc123",
|
|
SelfService: true,
|
|
InstallScript: installScript,
|
|
UninstallScript: uninstallScript,
|
|
PostInstallScript: postInstallScript,
|
|
PreInstallQuery: preInstallQuery,
|
|
Configuration: appConfiguration,
|
|
}}, nil
|
|
}
|
|
ds.GetSoftwareInstallerMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint, withScriptContents bool) (*fleet.SoftwareInstaller, error) {
|
|
return &fleet.SoftwareInstaller{InstallerID: 1, DisplayName: "Foo"}, nil
|
|
}
|
|
ds.GetCategoriesForSoftwareInstallersFunc = func(ctx context.Context, installerIDs []uint) (map[uint][]string, error) {
|
|
return nil, nil
|
|
}
|
|
ds.GetSummaryHostSoftwareInstallsFunc = func(ctx context.Context, installerID uint) (*fleet.SoftwareInstallerStatusSummary, error) {
|
|
return &fleet.SoftwareInstallerStatusSummary{}, nil
|
|
}
|
|
ds.GetVPPAppMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint) (*fleet.VPPAppStoreApp, error) {
|
|
return &fleet.VPPAppStoreApp{Name: "Bar", Configuration: appConfiguration}, nil
|
|
}
|
|
ds.GetSummaryHostVPPAppInstallsFunc = func(ctx context.Context, teamID *uint, appID fleet.VPPAppID) (*fleet.VPPAppStatusSummary, error) {
|
|
return &fleet.VPPAppStatusSummary{}, nil
|
|
}
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
user *fleet.User
|
|
// canReadInstaller is whether the role holds installable_entity read.
|
|
canReadInstaller bool
|
|
// installersWithoutTeam is whether omitting the fleet still returns
|
|
// installer data, which needs read on the "no team" scope it resolves to.
|
|
installersWithoutTeam bool
|
|
}{
|
|
{
|
|
name: "global-admin",
|
|
user: &fleet.User{ID: 1, GlobalRole: new(fleet.RoleAdmin)},
|
|
canReadInstaller: true,
|
|
installersWithoutTeam: true,
|
|
},
|
|
{
|
|
name: "global-maintainer",
|
|
user: &fleet.User{ID: 1, GlobalRole: new(fleet.RoleMaintainer)},
|
|
canReadInstaller: true,
|
|
installersWithoutTeam: true,
|
|
},
|
|
{
|
|
// Technician is the role that separates an installable_entity check
|
|
// from a hardcoded admin/maintainer list: it reads installers but is
|
|
// neither.
|
|
name: "global-technician",
|
|
user: &fleet.User{ID: 1, GlobalRole: new(fleet.RoleTechnician)},
|
|
canReadInstaller: true,
|
|
installersWithoutTeam: true,
|
|
},
|
|
{
|
|
name: "global-observer",
|
|
user: &fleet.User{ID: 1, GlobalRole: new(fleet.RoleObserver)},
|
|
canReadInstaller: false,
|
|
installersWithoutTeam: true,
|
|
},
|
|
{
|
|
name: "global-observer-plus",
|
|
user: &fleet.User{ID: 1, GlobalRole: new(fleet.RoleObserverPlus)},
|
|
canReadInstaller: false,
|
|
installersWithoutTeam: true,
|
|
},
|
|
{
|
|
name: "team-admin",
|
|
user: &fleet.User{ID: 1, Teams: []fleet.UserTeam{{
|
|
Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin,
|
|
}}},
|
|
canReadInstaller: true,
|
|
installersWithoutTeam: false,
|
|
},
|
|
{
|
|
name: "team-maintainer",
|
|
user: &fleet.User{ID: 1, Teams: []fleet.UserTeam{{
|
|
Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer,
|
|
}}},
|
|
canReadInstaller: true,
|
|
installersWithoutTeam: false,
|
|
},
|
|
{
|
|
name: "team-technician",
|
|
user: &fleet.User{ID: 1, Teams: []fleet.UserTeam{{
|
|
Team: fleet.Team{ID: 1}, Role: fleet.RoleTechnician,
|
|
}}},
|
|
canReadInstaller: true,
|
|
installersWithoutTeam: false,
|
|
},
|
|
{
|
|
name: "team-observer",
|
|
user: &fleet.User{ID: 1, Teams: []fleet.UserTeam{{
|
|
Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver,
|
|
}}},
|
|
canReadInstaller: false,
|
|
installersWithoutTeam: false,
|
|
},
|
|
{
|
|
name: "team-observer-plus",
|
|
user: &fleet.User{ID: 1, Teams: []fleet.UserTeam{{
|
|
Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus,
|
|
}}},
|
|
canReadInstaller: false,
|
|
installersWithoutTeam: false,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tc.user})
|
|
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
title, err := svc.SoftwareTitleByID(ctx, 1, new(uint(1)))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, title.SoftwarePackage)
|
|
require.Len(t, title.Packages, 1)
|
|
require.NotNil(t, title.AppStoreApp)
|
|
|
|
// Both the single package and the packages list are serialized.
|
|
for _, pkg := range []*fleet.SoftwareInstaller{title.SoftwarePackage, &title.Packages[0]} {
|
|
if tc.canReadInstaller {
|
|
require.Equal(t, installScript, pkg.InstallScript)
|
|
require.Equal(t, uninstallScript, pkg.UninstallScript)
|
|
require.Equal(t, postInstallScript, pkg.PostInstallScript)
|
|
require.Equal(t, preInstallQuery, pkg.PreInstallQuery)
|
|
require.NotEmpty(t, pkg.Configuration)
|
|
} else {
|
|
require.Empty(t, pkg.InstallScript)
|
|
require.Empty(t, pkg.UninstallScript)
|
|
require.Empty(t, pkg.PostInstallScript)
|
|
require.Empty(t, pkg.PreInstallQuery)
|
|
require.Empty(t, pkg.Configuration)
|
|
}
|
|
|
|
// Filtering the whole package would be a regression.
|
|
require.Equal(t, "foo.pkg", pkg.Name)
|
|
require.Equal(t, "1.0", pkg.Version)
|
|
require.Equal(t, "darwin", pkg.Platform)
|
|
require.Equal(t, "abc123", pkg.StorageID)
|
|
require.True(t, pkg.SelfService)
|
|
require.NotNil(t, pkg.Status)
|
|
}
|
|
|
|
if tc.canReadInstaller {
|
|
require.NotEmpty(t, title.AppStoreApp.Configuration)
|
|
} else {
|
|
require.Empty(t, title.AppStoreApp.Configuration)
|
|
}
|
|
require.Equal(t, "Bar", title.AppStoreApp.Name)
|
|
|
|
// Omitting the fleet must not fail, or a title's existence becomes
|
|
// guessable; the "no team" installer data is what gets withheld.
|
|
ds.TeamExistsFuncInvoked = false
|
|
noTeamTitle, err := svc.SoftwareTitleByID(ctx, 1, nil)
|
|
require.NoError(t, err)
|
|
require.False(t, ds.TeamExistsFuncInvoked)
|
|
if tc.installersWithoutTeam {
|
|
require.NotNil(t, noTeamTitle.SoftwarePackage)
|
|
require.Len(t, noTeamTitle.Packages, 1)
|
|
} else {
|
|
require.Nil(t, noTeamTitle.SoftwarePackage)
|
|
require.Empty(t, noTeamTitle.Packages)
|
|
require.Nil(t, noTeamTitle.AppStoreApp)
|
|
}
|
|
|
|
// An explicit "no team" is authorized up front, so it still fails.
|
|
_, err = svc.SoftwareTitleByID(ctx, 1, new(uint(0)))
|
|
checkAuthErr(t, !tc.installersWithoutTeam, err)
|
|
require.False(t, ds.TeamExistsFuncInvoked)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSoftwareNameUpdate(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ds.SoftwareTitleByIDFunc = func(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
|
|
return nil, ¬FoundError{}
|
|
}
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{
|
|
ID: 1,
|
|
GlobalRole: ptr.String(fleet.RoleAdmin),
|
|
}})
|
|
|
|
// Title not found
|
|
err := svc.UpdateSoftwareName(ctx, 1, "2 Chrome 2 Furious")
|
|
require.ErrorContains(t, err, "not found")
|
|
require.False(t, ds.UpdateHostSoftwareFuncInvoked)
|
|
|
|
// Title found but doesn't have a bundle ID
|
|
title := &fleet.SoftwareTitle{}
|
|
ds.SoftwareTitleByIDFunc = func(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
|
|
return title, nil
|
|
}
|
|
err = svc.UpdateSoftwareName(ctx, 1, "2 Chrome 2 Furious")
|
|
require.ErrorContains(t, err, "bundle")
|
|
require.False(t, ds.UpdateHostSoftwareFuncInvoked)
|
|
|
|
// Title found with bundle ID but user didn't provide a name
|
|
title = &fleet.SoftwareTitle{BundleIdentifier: ptr.String("foo")}
|
|
err = svc.UpdateSoftwareName(ctx, 1, "")
|
|
require.ErrorContains(t, err, "name")
|
|
require.False(t, ds.UpdateHostSoftwareFuncInvoked)
|
|
|
|
// Success case
|
|
ds.UpdateSoftwareTitleNameFunc = func(ctx context.Context, id uint, name string) error {
|
|
return nil
|
|
}
|
|
err = svc.UpdateSoftwareName(ctx, 1, "2 Chrome 2 Furious")
|
|
require.NoError(t, err)
|
|
require.True(t, ds.UpdateSoftwareTitleNameFuncInvoked)
|
|
}
|
|
|
|
func TestSoftwareTitleByIDTeamIDZero(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ds.SoftwareTitleByIDFunc = func(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
|
|
return &fleet.SoftwareTitle{BundleIdentifier: new("com.example.app")}, nil
|
|
}
|
|
ds.TeamExistsFunc = func(ctx context.Context, teamID uint) (bool, error) { return true, nil }
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
|
|
teamIDZero := new(uint) // *uint pointing to 0
|
|
|
|
// Team-scoped user on team 1 should not be able to access software with team_id=0
|
|
teamUser := &fleet.User{
|
|
ID: 1,
|
|
Teams: []fleet.UserTeam{{
|
|
Team: fleet.Team{ID: 1},
|
|
Role: fleet.RoleAdmin,
|
|
}},
|
|
}
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: teamUser})
|
|
|
|
_, err := svc.SoftwareTitleByID(ctx, 1, teamIDZero)
|
|
checkAuthErr(t, true, err)
|
|
|
|
// Global admin should still be able to access software with team_id=0
|
|
globalAdmin := &fleet.User{
|
|
ID: 2,
|
|
GlobalRole: new(fleet.RoleAdmin),
|
|
}
|
|
adminCtx := viewer.NewContext(ctx, viewer.Viewer{User: globalAdmin})
|
|
|
|
_, err = svc.SoftwareTitleByID(adminCtx, 1, teamIDZero)
|
|
checkAuthErr(t, false, err)
|
|
}
|
|
|
|
func TestSoftwareTitleByIDNilTeamIDExistsElsewhere(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
var filtersUsed []fleet.TeamFilter
|
|
ds.SoftwareTitleByIDFunc = func(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
|
|
filtersUsed = append(filtersUsed, tmFilter)
|
|
return nil, newNotFoundError()
|
|
}
|
|
|
|
svc, ctx := newTestService(t, ds, nil, nil)
|
|
teamUser := &fleet.User{
|
|
ID: 1,
|
|
Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}},
|
|
}
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: teamUser})
|
|
|
|
_, err := svc.SoftwareTitleByID(ctx, 1, nil)
|
|
require.Error(t, err)
|
|
// A title's existence on a team the caller can't see must not be
|
|
// distinguishable (via status code) from it not existing at all.
|
|
require.True(t, fleet.IsNotFound(err), "expected NotFound, got: %v", err)
|
|
|
|
// Guard against reintroducing a secondary lookup: exactly one call, and
|
|
// never with a global-role filter standing in for the real caller.
|
|
require.Len(t, filtersUsed, 1)
|
|
require.Equal(t, teamUser, filtersUsed[0].User)
|
|
}
|