Immediately reject duplicate Android web-clips (#42704)

Fixes #42700
This commit is contained in:
Carlo
2026-03-31 09:34:12 -04:00
committed by GitHub
parent 93a782ab61
commit 8ca6ae1ca3
6 changed files with 77 additions and 50 deletions
+13 -5
View File
@@ -2744,13 +2744,21 @@ ORDER BY
return nil
}
func (ds *Datastore) CheckAndroidWebAppNameExists(ctx context.Context, name string) (bool, error) {
func (ds *Datastore) CheckAndroidWebAppNameExistsOnTeam(ctx context.Context, teamID *uint, name string, excludeAdamID string) (bool, error) {
globalOrTeamID := ptr.ValOrZero(teamID)
var exists bool
err := sqlx.GetContext(ctx, ds.reader(ctx), &exists,
`SELECT EXISTS(SELECT 1 FROM vpp_apps WHERE name = ? AND adam_id LIKE ? AND platform = 'android')`,
name, fleet.AndroidWebAppPrefix+"%")
err := sqlx.GetContext(ctx, ds.reader(ctx), &exists, `
SELECT EXISTS(
SELECT 1 FROM vpp_apps va
JOIN vpp_apps_teams vat ON va.adam_id = vat.adam_id AND va.platform = vat.platform
WHERE va.name = ?
AND va.adam_id LIKE ?
AND va.adam_id != ?
AND va.platform = 'android'
AND vat.global_or_team_id = ?
)`, name, fleet.AndroidWebAppPrefix+"%", excludeAdamID, globalOrTeamID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "checking android web app name exists")
return false, ctxerr.Wrap(ctx, err, "checking android web app name exists on team")
}
return exists, nil
}
+4 -3
View File
@@ -767,9 +767,10 @@ type Datastore interface {
CheckConflictingInstallerExists(ctx context.Context, teamID *uint, bundleIdentifier, platform string) (bool, error)
CheckConflictingInHouseAppExists(ctx context.Context, teamID *uint, bundleIdentifier, platform string) (bool, error)
// CheckAndroidWebAppNameExists checks if an Android web app with the given
// name already exists in the vpp_apps table (fleet-wide).
CheckAndroidWebAppNameExists(ctx context.Context, name string) (bool, error)
// CheckAndroidWebAppNameExistsOnTeam checks if a different Android web app
// with the given name already exists on the specified team (via vpp_apps_teams + vpp_apps).
// The excludeAdamID param excludes the app being added/updated from the check.
CheckAndroidWebAppNameExistsOnTeam(ctx context.Context, teamID *uint, name string, excludeAdamID string) (bool, error)
///////////////////////////////////////////////////////////////////////////////
// OperatingSystemsStore
+6 -6
View File
@@ -579,7 +579,7 @@ type CheckConflictingInstallerExistsFunc func(ctx context.Context, teamID *uint,
type CheckConflictingInHouseAppExistsFunc func(ctx context.Context, teamID *uint, bundleIdentifier string, platform string) (bool, error)
type CheckAndroidWebAppNameExistsFunc func(ctx context.Context, name string) (bool, error)
type CheckAndroidWebAppNameExistsOnTeamFunc func(ctx context.Context, teamID *uint, name string, excludeAdamID string) (bool, error)
type GetHostOperatingSystemFunc func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error)
@@ -2678,8 +2678,8 @@ type DataStore struct {
CheckConflictingInHouseAppExistsFunc CheckConflictingInHouseAppExistsFunc
CheckConflictingInHouseAppExistsFuncInvoked bool
CheckAndroidWebAppNameExistsFunc CheckAndroidWebAppNameExistsFunc
CheckAndroidWebAppNameExistsFuncInvoked bool
CheckAndroidWebAppNameExistsOnTeamFunc CheckAndroidWebAppNameExistsOnTeamFunc
CheckAndroidWebAppNameExistsOnTeamFuncInvoked bool
GetHostOperatingSystemFunc GetHostOperatingSystemFunc
GetHostOperatingSystemFuncInvoked bool
@@ -6523,11 +6523,11 @@ func (s *DataStore) CheckConflictingInHouseAppExists(ctx context.Context, teamID
return s.CheckConflictingInHouseAppExistsFunc(ctx, teamID, bundleIdentifier, platform)
}
func (s *DataStore) CheckAndroidWebAppNameExists(ctx context.Context, name string) (bool, error) {
func (s *DataStore) CheckAndroidWebAppNameExistsOnTeam(ctx context.Context, teamID *uint, name string, excludeAdamID string) (bool, error) {
s.mu.Lock()
s.CheckAndroidWebAppNameExistsFuncInvoked = true
s.CheckAndroidWebAppNameExistsOnTeamFuncInvoked = true
s.mu.Unlock()
return s.CheckAndroidWebAppNameExistsFunc(ctx, name)
return s.CheckAndroidWebAppNameExistsOnTeamFunc(ctx, teamID, name, excludeAdamID)
}
func (s *DataStore) GetHostOperatingSystem(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) {
@@ -1133,7 +1133,6 @@ func (s *integrationMDMTestSuite) TestAndroidWebApps() {
id := uuid.NewString()
return &androidmanagement.WebApp{Name: fmt.Sprintf("enterprises/%s/webApps/%s", enterpriseID, id)}, nil
}
cases := []struct {
desc string
title string
@@ -1352,42 +1351,51 @@ func (s *integrationMDMTestSuite) TestAndroidWebAppsDuplicateName() {
return &androidmanagement.Application{IconUrl: "https://example.com/icon.jpg", Title: "Duplicate Web App"}, nil
}
// create a web app
// create two web apps with the same title (this is fine — POST /web_apps is just a Google API wrapper)
body, headers := generateMultipartRequest(t, "", "", nil, s.token, map[string][]string{
"title": {"Duplicate Web App"},
"url": {"https://example.com"},
})
var resp createAndroidWebAppResponse
var resp1 createAndroidWebAppResponse
res := s.DoRawWithHeaders("POST", "/api/latest/fleet/software/web_apps", body.Bytes(), http.StatusOK, headers)
err = json.NewDecoder(res.Body).Decode(&resp)
err = json.NewDecoder(res.Body).Decode(&resp1)
require.NoError(t, err)
webAppID := resp.AppStoreID
webAppID1 := resp1.AppStoreID
// add it to Fleet (populates vpp_apps)
var addResp addAppStoreAppResponse
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
AppStoreID: webAppID, Platform: fleet.AndroidPlatform,
}, http.StatusOK, &addResp)
// create another web app with the same name
body, headers = generateMultipartRequest(t, "", "", nil, s.token, map[string][]string{
"title": {"Duplicate Web App"},
"url": {"https://different-url.com"},
})
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/software/web_apps", body.Bytes(), http.StatusConflict, headers)
var resp2 createAndroidWebAppResponse
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/software/web_apps", body.Bytes(), http.StatusOK, headers)
err = json.NewDecoder(res.Body).Decode(&resp2)
require.NoError(t, err)
webAppID2 := resp2.AppStoreID
// create a team to add the apps to
tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name()})
require.NoError(t, err)
// add the first web app to the team
var addResp addAppStoreAppResponse
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
AppStoreID: webAppID1, Platform: fleet.AndroidPlatform, TeamID: &tm.ID,
}, http.StatusOK, &addResp)
// add the second web app (same name) to the same team
res = s.Do("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
AppStoreID: webAppID2, Platform: fleet.AndroidPlatform, TeamID: &tm.ID,
}, http.StatusConflict)
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, `Couldn't add.`)
require.Contains(t, errMsg, `"Duplicate Web App"`)
require.Contains(t, errMsg, "already exists in this fleet")
// create a web app with a different name
body, headers = generateMultipartRequest(t, "", "", nil, s.token, map[string][]string{
"title": {"Different Web App"},
"url": {"https://example.com"},
})
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/software/web_apps", body.Bytes(), http.StatusOK, headers)
var resp2 createAndroidWebAppResponse
err = json.NewDecoder(res.Body).Decode(&resp2)
// add the second web app (same name, rejected from team 1) to a different team
tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "2"})
require.NoError(t, err)
require.NotEmpty(t, resp2.AppStoreID)
var addResp2 addAppStoreAppResponse
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
AppStoreID: webAppID2, Platform: fleet.AndroidPlatform, TeamID: &tm2.ID,
}, http.StatusOK, &addResp2)
}
-4
View File
@@ -117,10 +117,6 @@ func TestVPPAuth(t *testing.T) {
ds.GetEnterpriseFunc = func(ctx context.Context) (*android.Enterprise, error) {
return &android.Enterprise{}, nil
}
ds.CheckAndroidWebAppNameExistsFunc = func(ctx context.Context, name string) (bool, error) {
return false, nil
}
// Note: these calls always return an error because they're attempting to unmarshal a
// non-existent VPP token.
_, err := svc.GetAppStoreApps(ctx, tt.teamID)