Skip to content

Commit

Permalink
feat: validate profile with apple (#21862)
Browse files Browse the repository at this point in the history
> Related issue: #17558

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [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/Committing-Changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
  • Loading branch information
jahzielv authored Sep 10, 2024
1 parent 9fedb59 commit 385da24
Show file tree
Hide file tree
Showing 12 changed files with 537 additions and 47 deletions.
2 changes: 2 additions & 0 deletions changes/17558-validation-errs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Adds validation of Setup Assistant profiles on profile upload, giving users immediate feedback on
the validity of the profile.
66 changes: 65 additions & 1 deletion cmd/fleetctl/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
"github.com/fleetdm/fleet/v4/server/mock"
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/google/uuid"
Expand Down Expand Up @@ -1171,13 +1173,17 @@ func TestApplyAsGitOps(t *testing.T) {
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
fleetCfg := config.TestConfig()
// Mock Apple DEP API
depStorage := SetupMockDEPStorageAndMockDEPServer(t)

config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, "../../server/service/testdata")

_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
License: license,
MDMStorage: enqueuer,
MDMPusher: mockPusher{},
FleetConfig: &fleetCfg,
DEPStorage: depStorage,
})

gitOps := &fleet.User{
Expand Down Expand Up @@ -1296,6 +1302,20 @@ func TestApplyAsGitOps(t *testing.T) {
return nil
}

ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil
}
ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
return 0, nil
}

ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) {
return []string{"foobar"}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{{ID: 1}}, nil
}

// Apply global config.
name := writeTmpYml(t, `---
apiVersion: v1
Expand Down Expand Up @@ -1632,6 +1652,34 @@ spec:
assert.Equal(t, "select * from app_schemes;", appliedQueries[0].Query)
}

func SetupMockDEPStorageAndMockDEPServer(t *testing.T) *nanodep_mock.Storage {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/server/devices"):
_, err := w.Write([]byte("{}"))
require.NoError(t, err)
case strings.Contains(r.URL.Path, "/session"):
_, err := w.Write([]byte(`{"auth_session_token": "yoo"}`))
require.NoError(t, err)
case strings.Contains(r.URL.Path, "/profile"):
_, err := w.Write([]byte(`{"profile_uuid": "profile123"}`))
require.NoError(t, err)
}
}))
depStorage := &nanodep_mock.Storage{}
depStorage.RetrieveConfigFunc = func(context.Context, string) (*nanodep_client.Config, error) {
return &nanodep_client.Config{
BaseURL: ts.URL,
}, nil
}
depStorage.RetrieveAuthTokensFunc = func(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) {
return &nanodep_client.OAuth1Tokens{}, nil
}
t.Cleanup(func() { ts.Close() })

return depStorage
}

func TestApplyEnrollSecrets(t *testing.T) {
_, ds := runServerWithMockedDS(t)

Expand Down Expand Up @@ -1885,7 +1933,8 @@ func TestApplyMacosSetup(t *testing.T) {
tier = fleet.TierPremium
}
license := &fleet.LicenseInfo{Tier: tier, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license})
depStorage := SetupMockDEPStorageAndMockDEPServer(t)
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license, DEPStorage: depStorage})

tm1 := &fleet.Team{ID: 1, Name: "tm1"}
teamsByName := map[string]*fleet.Team{
Expand Down Expand Up @@ -2027,6 +2076,21 @@ func TestApplyMacosSetup(t *testing.T) {
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
return nil, nil
}

ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil
}
ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
return 0, nil
}

ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) {
return []string{"foobar"}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{{ID: 1}}, nil
}

return ds
}

Expand Down
10 changes: 9 additions & 1 deletion ee/server/service/mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,8 +510,10 @@ func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst
// enabled (either globally or for a specific team, if provided)
var endUserAuthEnabled bool
var teamName *string
var tm *fleet.Team
if asst.TeamID != nil {
tm, err := svc.ds.Team(ctx, *asst.TeamID)
var err error
tm, err = svc.ds.Team(ctx, *asst.TeamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get team")
}
Expand Down Expand Up @@ -540,6 +542,12 @@ func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", `Couldn't edit macos_setup_assistant. The profile can't include "await_device_configured" option.`))
}

// Validate the profile with Apple's API. Don't save the profile if it isn't valid.
err := svc.depService.ValidateSetupAssistant(ctx, tm, asst, "")
if err != nil {
return nil, fleet.NewInvalidArgumentError("profile", err.Error())
}

// must read the existing setup assistant first to detect if it did change
// (so that the changed activity is not created if the same assistant was
// uploaded).
Expand Down
46 changes: 42 additions & 4 deletions ee/server/service/mdm_external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

Expand All @@ -18,7 +21,7 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
nanodep_storage "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mock"
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/fleetdm/fleet/v4/server/ptr"
Expand All @@ -30,7 +33,7 @@ import (
"github.com/stretchr/testify/require"
)

func setupMockDatastorePremiumService() (*mock.Store, *eeservice.Service, context.Context) {
func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Service, context.Context) {
ds := new(mock.Store)
lic := &fleet.LicenseInfo{Tier: fleet.TierPremium}
ctx := license.NewContext(context.Background(), lic)
Expand All @@ -42,7 +45,29 @@ func setupMockDatastorePremiumService() (*mock.Store, *eeservice.Service, contex
AppleSCEPKeyBytes: eeservice.TestKey,
},
}
var depStorage nanodep_storage.AllDEPStorage = &nanodep_mock.Storage{}
depStorage := &nanodep_mock.Storage{}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/server/devices"):
_, err := w.Write([]byte("{}"))
require.NoError(t, err)
case strings.Contains(r.URL.Path, "/session"):
_, err := w.Write([]byte(`{"auth_session_token": "yoo"}`))
require.NoError(t, err)
case strings.Contains(r.URL.Path, "/profile"):
_, err := w.Write([]byte(`{"profile_uuid": "profile123"}`))
require.NoError(t, err)
}
}))
depStorage.RetrieveConfigFunc = func(context.Context, string) (*nanodep_client.Config, error) {
return &nanodep_client.Config{
BaseURL: ts.URL,
}, nil
}
depStorage.RetrieveAuthTokensFunc = func(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) {
return &nanodep_client.OAuth1Tokens{}, nil
}
t.Cleanup(func() { ts.Close() })

freeSvc, err := service.NewService(
ctx,
Expand Down Expand Up @@ -92,7 +117,7 @@ func setupMockDatastorePremiumService() (*mock.Store, *eeservice.Service, contex
}

func TestGetOrCreatePreassignTeam(t *testing.T) {
ds, svc, ctx := setupMockDatastorePremiumService()
ds, svc, ctx := setupMockDatastorePremiumService(t)

ssoSettings := fleet.SSOProviderSettings{
EntityID: "foo",
Expand Down Expand Up @@ -219,6 +244,19 @@ func TestGetOrCreatePreassignTeam(t *testing.T) {
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: keyPEM},
}, nil
}

ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil
}
ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) {
return []string{"foobar"}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{{ID: 1}}, nil
}
ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
return 0, nil
}
}

authzCtx := &authz_ctx.AuthorizationContext{}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IApiError } from "interfaces/errors";
import { NotificationContext } from "context/notification";
import mdmAPI from "services/entities/mdm";

import CustomLink from "components/CustomLink";
import FileUploader from "components/FileUploader";

import { getErrorMessage } from "./helpers";
Expand Down Expand Up @@ -40,7 +41,23 @@ const SetupAssistantProfileUploader = ({
} catch (e) {
const error = e as AxiosResponse<IApiError>;
const errMessage = getErrorMessage(error);
renderFlash("error", errMessage);
let errComponent = <>{errMessage}</>;
if (errMessage.includes("Couldn't upload")) {
errComponent = (
<>
{errMessage}.{" "}
<CustomLink
url="https://fleetdm.com/learn-more-about/dep-profile"
text="Learn more"
className={`${baseClass}__new-tab`}
newTab
color="core-fleet-black"
iconColor="core-fleet-white"
/>
</>
);
}
renderFlash("error", errComponent);
} finally {
setShowLoading(false);
}
Expand Down
28 changes: 28 additions & 0 deletions server/datastore/mysql/testing_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,34 @@ func SetOrderedCreatedAtTimestamps(t testing.TB, ds *Datastore, afterTime time.T
return now
}

func CreateABMKeyCertIfNotExists(t testing.TB, ds *Datastore) {
certPEM, keyPEM, _, err := GenerateTestABMAssets(t)
require.NoError(t, err)
var assets []fleet.MDMConfigAsset
_, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{
fleet.MDMAssetABMKey,
})
if err != nil {
var nfe fleet.NotFoundError
require.ErrorAs(t, err, &nfe)
assets = append(assets, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMKey, Value: keyPEM})
}

_, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{
fleet.MDMAssetABMCert,
})
if err != nil {
var nfe fleet.NotFoundError
require.ErrorAs(t, err, &nfe)
assets = append(assets, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMCert, Value: certPEM})
}

if len(assets) != 0 {
err = ds.InsertMDMConfigAssets(context.Background(), assets)
require.NoError(t, err)
}
}

// CreateAndSetABMToken creates a new ABM token (using an existing ABM key/cert) and stores it in the DB.
func CreateAndSetABMToken(t testing.TB, ds *Datastore, orgName string) *fleet.ABMToken {
assets, err := ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{
Expand Down
Loading

0 comments on commit 385da24

Please sign in to comment.