+24
-10
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user