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
+24 -10
View File
@@ -318,6 +318,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
return nil, &fleet.BadRequestError{Message: "Android MDM is not enabled", InternalErr: err}
}
seenWebAppNames := make(map[string]bool)
for _, a := range incomingAndroidApps {
androidApp, err := svc.androidModule.EnterprisesApplications(ctx, enterprise.Name(), a.AdamID)
if err != nil {
@@ -327,6 +328,16 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
return nil, ctxerr.Wrap(ctx, err, "bulk add app store apps: check if android app exists")
}
if strings.HasPrefix(a.AdamID, fleet.AndroidWebAppPrefix) {
lowerTitle := strings.ToLower(androidApp.Title)
if seenWebAppNames[lowerTitle] {
return nil, fleet.ConflictError{
Message: fmt.Sprintf("Couldn't add. Web app with this name (%q) already exists in this fleet. Please add a web app with a different name or delete the existing app and try again.", androidApp.Title),
}
}
seenWebAppNames[lowerTitle] = true
}
appStoreApps = append(appStoreApps, &fleet.VPPApp{
VPPAppTeam: a,
BundleIdentifier: a.AdamID,
@@ -649,6 +660,18 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
TeamID: teamID,
}
if strings.HasPrefix(appID.AdamID, fleet.AndroidWebAppPrefix) {
exists, err := svc.ds.CheckAndroidWebAppNameExistsOnTeam(ctx, teamID, androidApp.Title, appID.AdamID)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "checking for duplicate android web app name")
}
if exists {
return 0, fleet.ConflictError{
Message: fmt.Sprintf("Couldn't add. Web app with this name (%q) already exists in this fleet. Please add a web app with a different name or delete the existing app and try again.", androidApp.Title),
}
}
}
default:
if isAndroidAppID {
return 0, fleet.NewInvalidArgumentError(
@@ -1300,16 +1323,6 @@ func (svc *Service) CreateAndroidWebApp(ctx context.Context, title, startURL str
}
}
exists, err := svc.ds.CheckAndroidWebAppNameExists(ctx, title)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "checking android web app name")
}
if exists {
return "", fleet.ConflictError{
Message: fmt.Sprintf(`Couldn't add. Web app with this name ("%s") already exists in this fleet. Please add a web app with a different name or delete the existing app and try again.`, title),
}
}
enterprise, err := svc.ds.GetEnterprise(ctx)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "get android enterprise")
@@ -1339,5 +1352,6 @@ func (svc *Service) CreateAndroidWebApp(ctx context.Context, title, startURL str
// not available to WebApps, we must know if somehow android changes how those get named.
svc.logger.ErrorContext(ctx, "created Android webApp does not have expected package name format", "package_name", createdApp.Name)
}
return packageName, nil
}
+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)