Prevent duplicate Android web-clip apps with the same name (#42664)

Fixes #42641.
This commit is contained in:
Carlo
2026-03-30 14:35:28 -04:00
committed by GitHub
parent 1765c13523
commit 8d63bf2bbe
6 changed files with 102 additions and 0 deletions
+10
View File
@@ -1300,6 +1300,16 @@ 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")
+11
View File
@@ -2743,3 +2743,14 @@ ORDER BY
}
return nil
}
func (ds *Datastore) CheckAndroidWebAppNameExists(ctx context.Context, name string) (bool, error) {
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+"%")
if err != nil {
return false, ctxerr.Wrap(ctx, err, "checking android web app name exists")
}
return exists, nil
}
+4
View File
@@ -760,6 +760,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)
///////////////////////////////////////////////////////////////////////////////
// OperatingSystemsStore
+12
View File
@@ -577,6 +577,8 @@ 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 GetHostOperatingSystemFunc func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error)
type ListOperatingSystemsFunc func(ctx context.Context) ([]fleet.OperatingSystem, error)
@@ -2671,6 +2673,9 @@ type DataStore struct {
CheckConflictingInHouseAppExistsFunc CheckConflictingInHouseAppExistsFunc
CheckConflictingInHouseAppExistsFuncInvoked bool
CheckAndroidWebAppNameExistsFunc CheckAndroidWebAppNameExistsFunc
CheckAndroidWebAppNameExistsFuncInvoked bool
GetHostOperatingSystemFunc GetHostOperatingSystemFunc
GetHostOperatingSystemFuncInvoked bool
@@ -6506,6 +6511,13 @@ 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) {
s.mu.Lock()
s.CheckAndroidWebAppNameExistsFuncInvoked = true
s.mu.Unlock()
return s.CheckAndroidWebAppNameExistsFunc(ctx, name)
}
func (s *DataStore) GetHostOperatingSystem(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) {
s.mu.Lock()
s.GetHostOperatingSystemFuncInvoked = true
@@ -1329,3 +1329,65 @@ func (s *integrationMDMTestSuite) TestAndroidWebAppsCannotSetConfiguration() {
}, http.StatusOK, &batchResp)
require.Len(t, batchResp.Apps, 2)
}
func (s *integrationMDMTestSuite) TestAndroidWebAppsDuplicateName() {
ctx := context.Background()
t := s.T()
s.setSkipWorkerJobs(t)
appConf, err := s.ds.AppConfig(ctx)
require.NoError(t, err)
appConf.MDM.AndroidEnabledAndConfigured = false
err = s.ds.SaveAppConfig(ctx, appConf)
require.NoError(t, err)
enterpriseID := s.enableAndroidMDM(t)
var count int
s.androidAPIClient.EnterprisesWebAppsCreateFunc = func(ctx context.Context, enterpriseName string, app *androidmanagement.WebApp) (*androidmanagement.WebApp, error) {
count++
id := "dup" + fmt.Sprint(count)
return &androidmanagement.WebApp{Name: fmt.Sprintf("enterprises/%s/webApps/com.google.enterprise.webapp.%s", enterpriseID, id)}, nil
}
s.androidAPIClient.EnterprisesApplicationsFunc = func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) {
return &androidmanagement.Application{IconUrl: "https://example.com/icon.jpg", Title: "Duplicate Web App"}, nil
}
// create a web app
body, headers := generateMultipartRequest(t, "", "", nil, s.token, map[string][]string{
"title": {"Duplicate Web App"},
"url": {"https://example.com"},
})
var resp createAndroidWebAppResponse
res := s.DoRawWithHeaders("POST", "/api/latest/fleet/software/web_apps", body.Bytes(), http.StatusOK, headers)
err = json.NewDecoder(res.Body).Decode(&resp)
require.NoError(t, err)
webAppID := resp.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)
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)
require.NoError(t, err)
require.NotEmpty(t, resp2.AppStoreID)
}
+3
View File
@@ -117,6 +117,9 @@ 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.