fleet/server/service/mdm_test.go
Lucas Manuel Rodriguez 59786c8c0e
Add new Technician role (#39564)
Resolves #38621, #38627, and #38623.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] Added/updated automated tests
- [X] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added a new premium-tier Technician role with read/write permissions
across teams, hosts, policies, queries, and configurations.
* License validation now prevents assigning premium roles on Fleet Free
editions.

* **Bug Fixes**
* Updated role-based access controls across team management pages to
properly restrict technician access.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-11 15:00:10 -03:00

2759 lines
90 KiB
Go

package service
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"database/sql"
"encoding/json"
"errors"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/config"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/scep/x509util"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestGetMDMApple(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierFree}
cfg := config.TestConfig()
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
certPEM, err := os.ReadFile("testdata/server.pem")
require.NoError(t, err)
keyPEM, err := os.ReadFile("testdata/server.key")
require.NoError(t, err)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
_ sqlx.QueryerContext,
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetAPNSCert: {Name: fleet.MDMAssetAPNSCert, Value: certPEM},
fleet.MDMAssetAPNSKey: {Name: fleet.MDMAssetAPNSKey, Value: keyPEM},
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: certPEM},
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: keyPEM},
}, nil
}
ctx = test.UserContext(ctx, test.UserAdmin)
got, err := svc.GetAppleMDM(ctx)
require.NoError(t, err)
// NOTE: to inspect the test certificate, you can use:
// openssl x509 -in ./server/service/testdata/server.pem -text -noout
require.Equal(t, &fleet.AppleMDM{
CommonName: "servq.groob.io",
SerialNumber: "1",
Issuer: "groob-ca",
RenewDate: time.Date(2017, 10, 24, 13, 11, 44, 0, time.UTC),
}, got)
}
func TestMDMAppleAuthorization(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
depStorage := new(nanodep_mock.Storage)
depSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
switch r.URL.Path {
case "/session":
_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
case "/account":
_, _ = w.Write([]byte(`{"admin_id": "abc", "org_name": "test_org"}`))
}
}))
t.Cleanup(depSrv.Close)
depStorage.RetrieveConfigFunc = func(p0 context.Context, p1 string) (*nanodep_client.Config, error) {
return &nanodep_client.Config{BaseURL: depSrv.URL}, nil
}
depStorage.RetrieveAuthTokensFunc = func(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) {
return &nanodep_client.OAuth1Tokens{}, nil
}
depStorage.StoreAssignerProfileFunc = func(ctx context.Context, name string, profileUUID string) error {
return nil
}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true, DEPStorage: depStorage})
ds.GetAllMDMConfigAssetsHashesFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) {
return map[fleet.MDMAssetName]string{
fleet.MDMAssetAPNSCert: "apnscert",
fleet.MDMAssetAPNSKey: "apnskey",
fleet.MDMAssetCACert: "scepcert",
fleet.MDMAssetCAKey: "scepkey",
}, nil
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
_ sqlx.QueryerContext,
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{}, nil
}
ds.InsertMDMConfigAssetsFunc = func(ctx context.Context, assets []fleet.MDMConfigAsset, _ sqlx.ExtContext) error { return nil }
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Nurv"}}, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) error {
return nil
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
return nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return nil, nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return nil, nil
}
ds.GetVPPTokenFunc = func(ctx context.Context, id uint) (*fleet.VPPTokenDB, error) {
return nil, &notFoundErr{}
}
ds.DeleteMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) error { return nil }
ds.MarkAllPendingAppleVPPAndInHouseInstallsAsFailedFunc = func(ctx context.Context, jobName string) error { return nil }
// use a custom implementation of checkAuthErr as the service call will fail
// with a not found error (given that MDM is not really configured) in case
// of success, and the package-wide checkAuthErr requires no error.
checkAuthErr := func(t *testing.T, shouldFail bool, err error) {
if shouldFail {
require.Error(t, err)
require.Equal(t, (&authz.Forbidden{}).Error(), err.Error())
} else if err != nil {
require.NotEqual(t, (&authz.Forbidden{}).Error(), err.Error())
}
}
testAuthdMethods := func(t *testing.T, user *fleet.User, shouldFailWithAuth bool) {
ctx := test.UserContext(ctx, user)
_, err := svc.GetAppleMDM(ctx)
checkAuthErr(t, shouldFailWithAuth, err)
_, err = svc.GetAppleBM(ctx)
checkAuthErr(t, shouldFailWithAuth, err)
// deliberately send invalid args so it doesn't actually generate a CSR
_, err = svc.RequestMDMAppleCSR(ctx, "not-an-email", "")
require.Error(t, err) // it *will* always fail, but not necessarily due to authorization
checkAuthErr(t, shouldFailWithAuth, err)
_, err = svc.GetMDMAppleCSR(ctx)
checkAuthErr(t, shouldFailWithAuth, err)
err = svc.UploadMDMAppleAPNSCert(ctx, nil)
require.Error(t, err)
checkAuthErr(t, shouldFailWithAuth, err)
err = svc.DeleteMDMAppleAPNSCert(ctx) // Don't expect anything other than an authz error here, since this is pretty much just a DB wrapper.
checkAuthErr(t, shouldFailWithAuth, err)
_, err = svc.UploadVPPToken(ctx, nil)
checkAuthErr(t, shouldFailWithAuth, err)
_, err = svc.GetVPPTokens(ctx)
checkAuthErr(t, shouldFailWithAuth, err)
err = svc.DeleteVPPToken(ctx, 0)
checkAuthErr(t, shouldFailWithAuth, err)
}
// Only global admins can access the endpoints.
testAuthdMethods(t, test.UserAdmin, false)
// All other users should not have access to the endpoints.
for _, user := range []*fleet.User{
test.UserNoRoles,
test.UserMaintainer,
test.UserObserver,
test.UserObserverPlus,
test.UserTeamAdminTeam1,
} {
testAuthdMethods(t, user, true)
}
}
func TestVerifyMDMAppleConfigured(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
cfg := config.TestConfig()
svc, baseCtx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
// mdm not configured
authzCtx := &authz_ctx.AuthorizationContext{}
ctx := authz_ctx.NewContext(baseCtx, authzCtx)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: false}}, nil
}
err := svc.VerifyMDMAppleConfigured(ctx)
require.ErrorIs(t, err, fleet.ErrMDMNotConfigured)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.True(t, authzCtx.Checked())
// error retrieving app config
authzCtx = &authz_ctx.AuthorizationContext{}
ctx = authz_ctx.NewContext(baseCtx, authzCtx)
testErr := errors.New("test err")
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return nil, testErr
}
err = svc.VerifyMDMAppleConfigured(ctx)
require.ErrorIs(t, err, testErr)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.True(t, authzCtx.Checked())
// mdm configured
authzCtx = &authz_ctx.AuthorizationContext{}
ctx = authz_ctx.NewContext(baseCtx, authzCtx)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
}
err = svc.VerifyMDMAppleConfigured(ctx)
require.NoError(t, err)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.False(t, authzCtx.Checked())
}
func TestVerifyMDMWindowsConfigured(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
cfg := config.TestConfig()
svc, baseCtx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
// mdm not configured
authzCtx := &authz_ctx.AuthorizationContext{}
ctx := authz_ctx.NewContext(baseCtx, authzCtx)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: false}}, nil
}
err := svc.VerifyMDMWindowsConfigured(ctx)
require.ErrorIs(t, err, fleet.ErrWindowsMDMNotConfigured)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.True(t, authzCtx.Checked())
// error retrieving app config
authzCtx = &authz_ctx.AuthorizationContext{}
ctx = authz_ctx.NewContext(baseCtx, authzCtx)
testErr := errors.New("test err")
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return nil, testErr
}
err = svc.VerifyMDMWindowsConfigured(ctx)
require.ErrorIs(t, err, testErr)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.True(t, authzCtx.Checked())
// mdm configured
authzCtx = &authz_ctx.AuthorizationContext{}
ctx = authz_ctx.NewContext(baseCtx, authzCtx)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: true}}, nil
}
err = svc.VerifyMDMWindowsConfigured(ctx)
require.NoError(t, err)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.False(t, authzCtx.Checked())
}
// TestVerifyAnyMDMConfigured validates the service helper that powers the
// middleware and ensures each Apple/Windows/Android configuration path is covered.
func TestVerifyAnyMDMConfigured(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
cfg := config.TestConfig()
svc, baseCtx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
// helper to create context per assertion
newCtx := func() (context.Context, *authz_ctx.AuthorizationContext) {
authzCtx := &authz_ctx.AuthorizationContext{}
return authz_ctx.NewContext(baseCtx, authzCtx), authzCtx
}
// none configured
ctx, authzCtx := newCtx()
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{}}, nil
}
err := svc.VerifyAnyMDMConfigured(ctx)
require.ErrorIs(t, err, fleet.ErrMDMNotConfigured)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.True(t, authzCtx.Checked())
// error retrieving config
ctx, authzCtx = newCtx()
testErr := errors.New("test err")
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return nil, testErr
}
err = svc.VerifyAnyMDMConfigured(ctx)
require.ErrorIs(t, err, testErr)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.True(t, authzCtx.Checked())
// apple only
ctx, authzCtx = newCtx()
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
}
err = svc.VerifyAnyMDMConfigured(ctx)
require.NoError(t, err)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.False(t, authzCtx.Checked())
// windows only
ctx, authzCtx = newCtx()
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: true}}, nil
}
err = svc.VerifyAnyMDMConfigured(ctx)
require.NoError(t, err)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.False(t, authzCtx.Checked())
// android only
ctx, authzCtx = newCtx()
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{AndroidEnabledAndConfigured: true}}, nil
}
err = svc.VerifyAnyMDMConfigured(ctx)
require.NoError(t, err)
require.True(t, ds.AppConfigFuncInvoked)
ds.AppConfigFuncInvoked = false
require.False(t, authzCtx.Checked())
// multiple configs
ctx, authzCtx = newCtx()
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: true,
AndroidEnabledAndConfigured: true,
}}, nil
}
err = svc.VerifyAnyMDMConfigured(ctx)
require.NoError(t, err)
require.True(t, ds.AppConfigFuncInvoked)
require.False(t, authzCtx.Checked())
}
func TestMicrosoftWSTEPConfig(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierFree}
ds.WSTEPNewSerialFunc = func(context.Context) (*big.Int, error) {
return big.NewInt(1337), nil
}
ds.WSTEPStoreCertificateFunc = func(ctx context.Context, name string, crt *x509.Certificate) error {
require.Equal(t, "test-client", name)
require.Equal(t, "test-client", crt.Subject.CommonName)
require.Equal(t, "Fleet", crt.Subject.OrganizationalUnit[0])
return nil
}
certPath := "testdata/server.pem"
keyPath := "testdata/server.key"
// sanity check that the test data is valid
wantCertPEM, err := os.ReadFile(certPath)
require.NoError(t, err)
wantKeyPEM, err := os.ReadFile(keyPath)
require.NoError(t, err)
// specify the test data in the server config
cfg := config.TestConfig()
cfg.MDM.WindowsWSTEPIdentityCert = certPath
cfg.MDM.WindowsWSTEPIdentityKey = keyPath
// check that config.MDM.MicrosoftWSTEP() returns the expected values
_, cfgCertPEM, cfgKeyPEM, err := cfg.MDM.MicrosoftWSTEP()
require.NoError(t, err)
require.NotEmpty(t, cfgCertPEM)
require.Equal(t, wantCertPEM, cfgCertPEM)
require.NotEmpty(t, cfgKeyPEM)
require.Equal(t, wantKeyPEM, cfgKeyPEM)
// start the test service
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
ctx = test.UserContext(ctx, test.UserAdmin)
// test CSR signing
clienPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
csrTemplate := x509util.CertificateRequest{
CertificateRequest: x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "test-cient",
},
SignatureAlgorithm: x509.SHA256WithRSA,
},
}
csrDerBytes, err := x509util.CreateCertificateRequest(rand.Reader, &csrTemplate, clienPrivateKey)
require.NoError(t, err)
csr, err := x509.ParseCertificateRequest(csrDerBytes)
require.NoError(t, err)
// test the service method
rawDER, _, err := svc.SignMDMMicrosoftClientCSR(ctx, "test-client", csr)
require.NoError(t, err)
require.True(t, ds.WSTEPNewSerialFuncInvoked)
require.True(t, ds.WSTEPStoreCertificateFuncInvoked)
// TODO: additional assertions on the signed certificate
parsedCert, err := x509.ParseCertificate(rawDER)
require.NoError(t, err)
require.Equal(t, "test-client", parsedCert.Subject.CommonName)
require.Equal(t, "Fleet", parsedCert.Subject.OrganizationalUnit[0])
}
func TestRunMDMCommandAuthz(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
singleUnenrolledHost := []*fleet.Host{{ID: 1, TeamID: ptr.Uint(1), UUID: "a", Platform: "darwin"}}
team1And2UnenrolledHosts := []*fleet.Host{{ID: 1, TeamID: ptr.Uint(1), UUID: "a"}, {ID: 2, TeamID: ptr.Uint(2), UUID: "b"}}
team2And3UnenrolledHosts := []*fleet.Host{{ID: 2, TeamID: ptr.Uint(2), UUID: "b"}, {ID: 3, TeamID: ptr.Uint(3), UUID: "c"}}
ds.AreHostsConnectedToFleetMDMFunc = func(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) {
res := make(map[string]bool, len(hosts))
for _, h := range hosts {
res[h.UUID] = true
}
return res, nil
}
userTeamMaintainerTeam1And2 := &fleet.User{
ID: 100,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 1},
Role: fleet.RoleMaintainer,
},
{
Team: fleet.Team{ID: 2},
Role: fleet.RoleMaintainer,
},
},
}
userTeamAdminTeam1And2 := &fleet.User{
ID: 101,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 1},
Role: fleet.RoleAdmin,
},
{
Team: fleet.Team{ID: 2},
Role: fleet.RoleAdmin,
},
},
}
userTeamAdminTeam1ObserverTeam2 := &fleet.User{
ID: 102,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 1},
Role: fleet.RoleAdmin,
},
{
Team: fleet.Team{ID: 2},
Role: fleet.RoleObserver,
},
},
}
checkAuthErr := func(t *testing.T, shouldFailWithAuth bool, err error) {
t.Helper()
if shouldFailWithAuth {
require.Error(t, err)
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
} else {
// call always fails, but due to the host not being enrolled in MDM
require.Error(t, err)
require.NotContains(t, err.Error(), authz.ForbiddenErrorMessage)
}
}
enqueueCmdCases := []struct {
desc string
user *fleet.User
hosts []*fleet.Host
shouldFailWithAuth bool
}{
{"no role", test.UserNoRoles, singleUnenrolledHost, true},
{"maintainer", test.UserMaintainer, singleUnenrolledHost, false},
{"admin", test.UserAdmin, singleUnenrolledHost, false},
{"observer", test.UserObserver, singleUnenrolledHost, true},
{"observer+", test.UserObserverPlus, singleUnenrolledHost, true},
{"gitops", test.UserGitOps, singleUnenrolledHost, false},
{"team 1 admin", test.UserTeamAdminTeam1, singleUnenrolledHost, false},
{"team 2 admin", test.UserTeamAdminTeam2, singleUnenrolledHost, true},
{"team 1 maintainer", test.UserTeamMaintainerTeam1, singleUnenrolledHost, false},
{"team 2 maintainer", test.UserTeamMaintainerTeam2, singleUnenrolledHost, true},
{"team 1 observer", test.UserTeamObserverTeam1, singleUnenrolledHost, true},
{"team 2 observer", test.UserTeamObserverTeam2, singleUnenrolledHost, true},
{"team 1 observer+", test.UserTeamObserverPlusTeam1, singleUnenrolledHost, true},
{"team 2 observer+", test.UserTeamObserverPlusTeam2, singleUnenrolledHost, true},
{"team 1 gitops", test.UserTeamGitOpsTeam1, singleUnenrolledHost, false},
{"team 2 gitops", test.UserTeamGitOpsTeam2, singleUnenrolledHost, true},
{"team 1 admin mix of teams", test.UserTeamAdminTeam1, team1And2UnenrolledHosts, true},
{"team 1 maintainer mix of teams", test.UserTeamMaintainerTeam1, team1And2UnenrolledHosts, true},
{"admin mix of teams", test.UserAdmin, team1And2UnenrolledHosts, false},
{"team 1 admin 2 other teams", test.UserTeamAdminTeam1, team2And3UnenrolledHosts, true},
{"team 1 maintainer 2 other teams", test.UserTeamMaintainerTeam1, team2And3UnenrolledHosts, true},
{"admin mix of teams", test.UserAdmin, team1And2UnenrolledHosts, false},
{"admin mix of 2 other teams", test.UserAdmin, team2And3UnenrolledHosts, false},
{"team 1 and 2 admin on allowed teams", userTeamAdminTeam1And2, team1And2UnenrolledHosts, false},
{"team 1 and 2 maintainer on allowed teams", userTeamMaintainerTeam1And2, team1And2UnenrolledHosts, false},
{"team 1 and 2 admin on other teams", userTeamAdminTeam1And2, team2And3UnenrolledHosts, true},
{"team 1 and 2 maintainer on other teams", userTeamMaintainerTeam1And2, team2And3UnenrolledHosts, true},
{"team 1 admin and 2 observer on team 1", userTeamAdminTeam1ObserverTeam2, singleUnenrolledHost, false},
{"team 1 admin and 2 observer on team 2 and 3", userTeamAdminTeam1ObserverTeam2, team2And3UnenrolledHosts, true},
{"team 1 admin and 2 observer on team 1 and 2", userTeamAdminTeam1ObserverTeam2, team1And2UnenrolledHosts, true},
}
for _, c := range enqueueCmdCases {
t.Run(c.desc, func(t *testing.T) {
ds.ListHostsLiteByUUIDsFunc = func(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) {
return c.hosts, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: true,
},
}, nil
}
ctx = test.UserContext(ctx, c.user)
_, err := svc.RunMDMCommand(ctx, "base64command", []string{"uuid"})
checkAuthErr(t, c.shouldFailWithAuth, err)
})
}
}
func TestRunMDMCommandValidations(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
enrolledMDMInfo := &fleet.HostMDM{Enrolled: true, InstalledFromDep: false, Name: fleet.WellKnownMDMFleet, IsServer: false}
singleUnenrolledHost := []*fleet.Host{{ID: 0xf1337, TeamID: ptr.Uint(1), UUID: "unenrolled"}}
differentPlatformsHosts := []*fleet.Host{
{ID: 1, UUID: "a", Platform: "darwin"},
{ID: 2, UUID: "b", Platform: "windows"},
}
linuxSingleHost := []*fleet.Host{{ID: 1, TeamID: ptr.Uint(1), UUID: "a", Platform: "linux"}}
windowsSingleHost := []*fleet.Host{{ID: 1, TeamID: ptr.Uint(1), UUID: "a", Platform: "windows"}}
macosSingleHost := []*fleet.Host{{ID: 1, TeamID: ptr.Uint(1), UUID: "a", Platform: "darwin"}}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
if hostID == 0xf1337 {
return nil, sql.ErrNoRows
}
return enrolledMDMInfo, nil
}
ds.AreHostsConnectedToFleetMDMFunc = func(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) {
res := make(map[string]bool, len(hosts))
for _, h := range hosts {
res[h.UUID] = h.ID != 0xf1337
}
return res, nil
}
cases := []struct {
desc string
hosts []*fleet.Host
mdmConfigured bool
wantErr string
}{
{"no hosts", []*fleet.Host{}, false, "No hosts targeted."},
{"unenrolled host", singleUnenrolledHost, false, "Can't run the MDM command because one or more hosts have MDM turned off."},
{"different platforms", differentPlatformsHosts, false, "All hosts must be on the same platform."},
{"invalid platform", linuxSingleHost, false, "Invalid platform."},
{"mdm not configured (windows)", windowsSingleHost, false, "Windows MDM isn't turned on."},
{"mdm not configured (macos)", macosSingleHost, false, "macOS MDM isn't turned on."},
{"invalid base64 encoding", macosSingleHost, true, "unable to decode base64 command"},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
ds.ListHostsLiteByUUIDsFunc = func(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) {
return c.hosts, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: c.mdmConfigured,
WindowsEnabledAndConfigured: c.mdmConfigured,
},
}, nil
}
ctx = test.UserContext(ctx, test.UserAdmin)
_, err := svc.RunMDMCommand(ctx, "!@#", []string{"unused for this test"})
require.Error(t, err)
require.ErrorContains(t, err, c.wantErr)
})
}
}
func TestMDMCommonAuthorization(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true, WindowsEnabledAndConfigured: true, AndroidEnabledAndConfigured: true}}, nil
}
ds.GetMDMAppleFileVaultSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleFileVaultSummary, error) {
return &fleet.MDMAppleFileVaultSummary{}, nil
}
ds.GetMDMWindowsBitLockerSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) {
return &fleet.MDMWindowsBitLockerSummary{}, nil
}
ds.GetMDMWindowsProfilesSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) {
return &fleet.MDMProfilesSummary{}, nil
}
ds.GetMDMAndroidProfilesSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) {
return &fleet.MDMProfilesSummary{}, nil
}
ds.GetLinuxDiskEncryptionSummaryFunc = func(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error) {
return fleet.MDMLinuxDiskEncryptionSummary{}, nil
}
ds.GetConfigEnableDiskEncryptionFunc = func(ctx context.Context, teamID *uint) (fleet.DiskEncryptionConfig, error) {
return fleet.DiskEncryptionConfig{}, nil
}
ds.AreHostsConnectedToFleetMDMFunc = func(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) {
res := make(map[string]bool, len(hosts))
for _, h := range hosts {
res[h.UUID] = true
}
return res, nil
}
ds.GetMDMAppleConfigProfileFunc = func(ctx context.Context, pid string) (*fleet.MDMAppleConfigProfile, error) {
var tid uint
if pid == fleet.MDMAppleProfileUUIDPrefix+"-team-1-profile" {
tid = 1
}
return &fleet.MDMAppleConfigProfile{
ProfileUUID: pid,
TeamID: &tid,
}, nil
}
ds.GetMDMAppleDeclarationFunc = func(ctx context.Context, did string) (*fleet.MDMAppleDeclaration, error) {
var tid uint
if did == fleet.MDMAppleDeclarationUUIDPrefix+"-team-1-declaration" {
tid = 1
}
return &fleet.MDMAppleDeclaration{
DeclarationUUID: did,
TeamID: &tid,
}, nil
}
ds.GetMDMAndroidConfigProfileFunc = func(ctx context.Context, pid string) (*fleet.MDMAndroidConfigProfile, error) {
var tid uint
if pid == fleet.MDMAndroidProfileUUIDPrefix+"-team-1-profile" {
tid = 1
}
return &fleet.MDMAndroidConfigProfile{
ProfileUUID: pid,
TeamID: &tid,
}, nil
}
ds.GetMDMWindowsConfigProfileFunc = func(ctx context.Context, pid string) (*fleet.MDMWindowsConfigProfile, error) {
var tid uint
if pid == fleet.MDMWindowsProfileUUIDPrefix+"-team-1-profile" {
tid = 1
}
return &fleet.MDMWindowsConfigProfile{
ProfileUUID: pid,
TeamID: &tid,
}, nil
}
ds.GetMDMConfigProfileStatusFunc = func(ctx context.Context, pid string) (fleet.MDMConfigProfileStatus, error) {
return fleet.MDMConfigProfileStatus{}, nil
}
mockTeamFuncWithUser := func(u *fleet.User) mock.TeamWithExtrasFunc {
return func(ctx context.Context, teamID uint) (*fleet.Team, error) {
if len(u.Teams) > 0 {
for _, t := range u.Teams {
if t.ID == teamID {
return &fleet.Team{ID: teamID, Users: []fleet.TeamUser{{User: *u, Role: t.Role}}}, nil
}
}
}
return &fleet.Team{}, nil
}
}
testCases := []struct {
name string
user *fleet.User
shouldFailGlobal bool
shouldFailTeam bool
}{
{
"global admin",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
false,
},
{
"global maintainer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
false,
false,
},
{
"global observer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
true,
true,
},
{
"team admin, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
true,
false,
},
{
"team admin, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
true,
true,
},
{
"team maintainer, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
true,
false,
},
{
"team maintainer, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
true,
true,
},
{
"team observer, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
true,
true,
},
{
"team observer, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
true,
true,
},
{
"user no roles",
&fleet.User{ID: 1337},
true,
true,
},
}
checkShouldFail := func(err error, shouldFail bool) {
if !shouldFail {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
}
}
for _, tt := range testCases {
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
ds.TeamWithExtrasFunc = mockTeamFuncWithUser(tt.user)
t.Run(tt.name, func(t *testing.T) {
// test authz for MDM summary endpoints (no team)
_, err := svc.GetMDMDiskEncryptionSummary(ctx, nil)
checkShouldFail(err, tt.shouldFailGlobal)
_, err = svc.GetMDMWindowsProfilesSummary(ctx, nil)
checkShouldFail(err, tt.shouldFailGlobal)
_, err = svc.GetMDMAndroidProfilesSummary(ctx, nil)
checkShouldFail(err, tt.shouldFailGlobal)
// Apple profile summary tested in apple_mdm_test.go so not tested here
_, err = svc.GetMDMConfigProfileStatus(ctx, fleet.MDMAppleProfileUUIDPrefix+"-no-team-profile")
checkShouldFail(err, tt.shouldFailGlobal)
_, err = svc.GetMDMConfigProfileStatus(ctx, fleet.MDMAppleDeclarationUUIDPrefix+"-no-team-declaration")
checkShouldFail(err, tt.shouldFailGlobal)
_, err = svc.GetMDMConfigProfileStatus(ctx, fleet.MDMAndroidProfileUUIDPrefix+"-no-team-profile")
checkShouldFail(err, tt.shouldFailGlobal)
_, err = svc.GetMDMConfigProfileStatus(ctx, fleet.MDMWindowsProfileUUIDPrefix+"-no-team-profile")
checkShouldFail(err, tt.shouldFailGlobal)
// test authz for MDM summary endpoints (team 1)
_, err = svc.GetMDMDiskEncryptionSummary(ctx, ptr.Uint(1))
checkShouldFail(err, tt.shouldFailTeam)
_, err = svc.GetMDMWindowsProfilesSummary(ctx, ptr.Uint(1))
checkShouldFail(err, tt.shouldFailTeam)
_, err = svc.GetMDMAndroidProfilesSummary(ctx, ptr.Uint(1))
checkShouldFail(err, tt.shouldFailTeam)
_, err = svc.GetMDMConfigProfileStatus(ctx, fleet.MDMAppleProfileUUIDPrefix+"-team-1-profile")
checkShouldFail(err, tt.shouldFailTeam)
_, err = svc.GetMDMConfigProfileStatus(ctx, fleet.MDMAppleDeclarationUUIDPrefix+"-team-1-declaration")
checkShouldFail(err, tt.shouldFailTeam)
_, err = svc.GetMDMConfigProfileStatus(ctx, fleet.MDMAndroidProfileUUIDPrefix+"-team-1-profile")
checkShouldFail(err, tt.shouldFailTeam)
_, err = svc.GetMDMConfigProfileStatus(ctx, fleet.MDMWindowsProfileUUIDPrefix+"-team-1-profile")
checkShouldFail(err, tt.shouldFailTeam)
})
}
}
func TestEnqueueWindowsMDMCommand(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
ds.MDMWindowsInsertCommandForHostsFunc = func(ctx context.Context, deviceIDs []string, cmd *fleet.MDMWindowsCommand) error {
return nil
}
ds.AreHostsConnectedToFleetMDMFunc = func(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) {
res := make(map[string]bool, len(hosts))
for _, h := range hosts {
res[h.UUID] = true
}
return res, nil
}
cases := []struct {
desc string
premium bool
xmlCmd string
wantErr string
wantReqType string
}{
{"invalid xml", false, `!!$$`, "The payload isn't valid XML", ""},
{"empty xml", false, ``, "The payload isn't valid XML", ""},
{"unrelated xml", false, `<Unrelated></Unrelated>`, "You can run only <Exec> command type", ""},
{"no command Exec", false, `<Exec></Exec>`, "You can run only a single <Exec> command", ""},
{"non-exec command", false, `
<Get>
<CmdID>1</CmdID>
<Item>
<Target>
<LocURI>./DevDetail/SwV</LocURI>
</Target>
</Item>
</Get>`, "You can run only <Exec> command type", ""},
{"multi-exec command", false, `
<Exec>
<CmdID>1</CmdID>
<Item>
<Target>
<LocURI>./DevDetail/SwV</LocURI>
</Target>
</Item>
<Item>
<Target>
<LocURI>./DevDetail/SwV2</LocURI>
</Target>
</Item>
</Exec>`, "You can run only a single <Exec> command", ""},
{"premium command, non premium license", false, `
<Exec>
<CmdID>1</CmdID>
<Item>
<Target>
<LocURI>./Device/Vendor/MSFT/RemoteWipe/doWipe</LocURI>
</Target>
</Item>
</Exec>`, "Requires Fleet Premium license", ""},
{"premium command, premium license", true, `
<Exec>
<CmdID>1</CmdID>
<Item>
<Target>
<LocURI>./Device/Vendor/MSFT/RemoteWipe/doWipe</LocURI>
</Target>
</Item>
</Exec>`, "", "./Device/Vendor/MSFT/RemoteWipe/doWipe"},
{"non-premium command", false, `
<Exec>
<CmdID>1</CmdID>
<Item>
<Target>
<LocURI>./FooBar</LocURI>
</Target>
</Item>
</Exec>`, "", "./FooBar"},
{"multi top-level Execs", false, `
<Exec>
<CmdID>1</CmdID>
<Item>
<Target>
<LocURI>./FooBar</LocURI>
</Target>
</Item>
</Exec>
<Exec>
<CmdID>2</CmdID>
<Item>
<Target>
<LocURI>./FooBar2</LocURI>
</Target>
</Item>
</Exec>`, "You can run only a single <Exec> command", ""},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
ctx = test.UserContext(ctx, test.UserAdmin)
if c.premium {
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
}
var svcImpl *Service
switch v := svc.(type) {
case validationMiddleware:
svcImpl = v.Service.(*Service)
case *Service:
svcImpl = v
}
res, err := svcImpl.enqueueMicrosoftMDMCommand(ctx, []byte(c.xmlCmd), []string{"uuid"})
if c.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, c.wantErr)
} else {
require.NoError(t, err)
require.NotEmpty(t, res.CommandUUID)
require.Equal(t, "windows", res.Platform)
require.Equal(t, c.wantReqType, res.RequestType)
}
})
}
}
func TestGetMDMDiskEncryptionSummary(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license})
ctx = test.UserContext(ctx, test.UserAdmin)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
}
ds.GetMDMAppleFileVaultSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleFileVaultSummary, error) {
require.Nil(t, teamID)
return &fleet.MDMAppleFileVaultSummary{Verified: 1, Verifying: 2, ActionRequired: 3, Failed: 4, Enforcing: 5, RemovingEnforcement: 6}, nil
}
ds.GetMDMWindowsBitLockerSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) {
require.Nil(t, teamID)
// Use default zeros verifying, action_required, or removing_enforcement
return &fleet.MDMWindowsBitLockerSummary{Verified: 7, Failed: 8, Enforcing: 9}, nil
}
ds.AreHostsConnectedToFleetMDMFunc = func(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) {
res := make(map[string]bool, len(hosts))
for _, h := range hosts {
res[h.UUID] = true
}
return res, nil
}
ds.GetLinuxDiskEncryptionSummaryFunc = func(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error) {
require.Nil(t, teamID)
return fleet.MDMLinuxDiskEncryptionSummary{Verified: 1, ActionRequired: 2, Failed: 3}, nil
}
ds.GetConfigEnableDiskEncryptionFunc = func(ctx context.Context, teamID *uint) (fleet.DiskEncryptionConfig, error) {
return fleet.DiskEncryptionConfig{Enabled: true}, nil
}
// Test that the summary properly combines the results of the two methods
des, err := svc.GetMDMDiskEncryptionSummary(ctx, nil)
require.NoError(t, err)
require.NotNil(t, des)
require.Equal(t, *des, fleet.MDMDiskEncryptionSummary{
Verified: fleet.MDMPlatformsCounts{
MacOS: 1,
Windows: 7,
Linux: 1,
},
Verifying: fleet.MDMPlatformsCounts{
MacOS: 2,
Windows: 0,
},
ActionRequired: fleet.MDMPlatformsCounts{
MacOS: 3,
Windows: 0,
Linux: 2,
},
Failed: fleet.MDMPlatformsCounts{
MacOS: 4,
Windows: 8,
Linux: 3,
},
Enforcing: fleet.MDMPlatformsCounts{
MacOS: 5,
Windows: 9,
},
RemovingEnforcement: fleet.MDMPlatformsCounts{
MacOS: 6,
Windows: 0,
},
})
}
// TODO: Add tests for Apple DDM authz?
func TestMDMWindowsConfigProfileAuthz(t *testing.T) {
ds := new(mock.Store)
// while the config profiles are not premium-only, teams are and we want to test with teams.
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
testCases := []struct {
name string
user *fleet.User
shouldFailGlobalRead bool
shouldFailTeamRead bool
shouldFailGlobalWrite bool
shouldFailTeamWrite bool
}{
{
"global admin",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
false,
false,
false,
},
{
"global maintainer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
false,
false,
false,
false,
},
{
"global observer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
true,
true,
true,
true,
},
{
"global observer+",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)},
true,
true,
true,
true,
},
{
// this is authorized because any logged-in user can read teams (the
// first authorization check) and then gitops have write-access the the
// profiles.
"global gitops",
&fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)},
false,
false,
false,
false,
},
{
"team admin, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
true,
false,
true,
false,
},
{
"team admin, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
true,
true,
true,
true,
},
{
"team maintainer, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
true,
false,
true,
false,
},
{
"team maintainer, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
true,
true,
true,
true,
},
{
"team observer, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
true,
true,
true,
true,
},
{
"team observer, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
true,
true,
true,
true,
},
{
"team observer+, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}},
true,
true,
true,
true,
},
{
"team observer+, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserverPlus}}},
true,
true,
true,
true,
},
{
// this is authorized because any logged-in user can read teams (the
// first authorization check) and then gitops have write-access the the
// profiles.
"team gitops, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
true,
false,
true,
false,
},
{
"team gitops, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleGitOps}}},
true,
true,
true,
true,
},
{
"user no roles",
&fleet.User{ID: 1337},
true,
true,
true,
true,
},
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: true,
},
}, nil
}
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
return nil
}
ds.GetMDMWindowsConfigProfileFunc = func(ctx context.Context, pid string) (*fleet.MDMWindowsConfigProfile, error) {
var tid uint
if pid == "team-1" {
tid = 1
}
return &fleet.MDMWindowsConfigProfile{
ProfileUUID: pid,
TeamID: &tid,
}, nil
}
ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
return &fleet.Team{ID: tid, Name: "team1"}, nil
}
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
return &fleet.TeamLite{ID: tid, Name: "team1"}, nil
}
ds.DeleteMDMWindowsConfigProfileFunc = func(ctx context.Context, profileUUID string) error {
return nil
}
ds.NewMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile, usesFleetVars []fleet.FleetVarName) (*fleet.MDMWindowsConfigProfile, error) {
return &cp, nil
}
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
return nil, nil, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string,
hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
return nil
}
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
return &fleet.GroupedCertificateAuthorities{}, nil
}
checkShouldFail := func(t *testing.T, err error, shouldFail bool) {
if !shouldFail {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
}
}
const winProfContent = `<Replace></Replace>`
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
// test authz get config profile (no team)
_, err := svc.GetMDMWindowsConfigProfile(ctx, "global")
checkShouldFail(t, err, tt.shouldFailGlobalRead)
// test authz get config profile (team 1)
_, err = svc.GetMDMWindowsConfigProfile(ctx, "team-1")
checkShouldFail(t, err, tt.shouldFailTeamRead)
// test authz list config profiles (no team)
_, _, err = svc.ListMDMConfigProfiles(ctx, nil, fleet.ListOptions{})
checkShouldFail(t, err, tt.shouldFailGlobalRead)
// test authz list config profiles (team 1)
_, _, err = svc.ListMDMConfigProfiles(ctx, ptr.Uint(1), fleet.ListOptions{})
checkShouldFail(t, err, tt.shouldFailTeamRead)
// test authz create new profile (no team)
_, err = svc.NewMDMWindowsConfigProfile(ctx, 0, "prof", []byte(winProfContent), nil, fleet.LabelsIncludeAll)
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
// test authz create new profile (team 1)
_, err = svc.NewMDMWindowsConfigProfile(ctx, 1, "prof", []byte(winProfContent), nil, fleet.LabelsIncludeAll)
checkShouldFail(t, err, tt.shouldFailTeamWrite)
// test authz delete config profile (no team)
err = svc.DeleteMDMWindowsConfigProfile(ctx, "global")
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
// test authz delete config profile (team 1)
err = svc.DeleteMDMWindowsConfigProfile(ctx, "team-1")
checkShouldFail(t, err, tt.shouldFailTeamWrite)
})
}
}
func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
ds := new(mock.Store)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
if tid != 1 {
return nil, &notFoundError{}
}
return &fleet.Team{ID: tid, Name: "team1"}, nil
}
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
return nil
}
ds.NewMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile, usesFleetVars []fleet.FleetVarName) (*fleet.MDMWindowsConfigProfile, error) {
if bytes.Contains(cp.SyncML, []byte("duplicate")) {
return nil, &alreadyExistsError{}
}
cp.ProfileUUID = uuid.New().String()
return &cp, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string,
hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) {
return document, nil
}
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
return nil
}
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
return &fleet.GroupedCertificateAuthorities{}, nil
}
cases := []struct {
desc string
tmID uint
profile string
mdmConfigured bool
wantErr string
}{
{"empty profile", 0, "", true, "The file should include valid XML."},
{"plist data", 0, string(mcBytesForTest("Foo", "Bar", "UUID")), true, "The file should include valid XML: processing instructions are not allowed."},
{"random non-xml data", 0, "\x00\x01\x02", true, "The file should include valid XML:"},
{"valid windows profile", 0, `<Replace></Replace>`, true, ""},
{"mdm not enabled", 0, `<Replace></Replace>`, false, "Windows MDM isn't turned on."},
{"duplicate profile name", 0, `<Replace>duplicate</Replace>`, true, "configuration profile with this name already exists"},
{"multiple Replace", 0, `<Replace>a</Replace><Replace>b</Replace>`, true, ""},
{"Replace and non-Replace", 0, `<Replace>a</Replace><Get>b</Get>`, true, "Windows configuration profiles can only have <Replace> or <Add> top level elements."},
{
"BitLocker profile", 0,
`<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/BitLocker/AllowStandardUserEncryption</LocURI></Target></Item></Replace>`, true,
syncml.DiskEncryptionProfileRestrictionErrMsg,
},
{"Windows updates profile", 0, `<Replace><Item><Target><LocURI> ./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineNoAutoRebootForFeatureUpdates </LocURI></Target></Item></Replace>`, true, "Custom configuration profiles can't include Windows updates settings."},
{"unsupported Fleet variable", 0, `<Replace>$FLEET_VAR_BOZO</Replace>`, true, "Fleet variable"},
{"team empty profile", 1, "", true, "The file should include valid XML."},
{"team plist data", 1, string(mcBytesForTest("Foo", "Bar", "UUID")), true, "The file should include valid XML: processing instructions are not allowed."},
{"team random non-xml data", 1, "\x00\x01\x02", true, "The file should include valid XML:"},
{"team valid windows profile", 1, `<Replace></Replace>`, true, ""},
{"team mdm not enabled", 1, `<Replace></Replace>`, false, "Windows MDM isn't turned on."},
{"team duplicate profile name", 1, `<Replace>duplicate</Replace>`, true, "configuration profile with this name already exists"},
{"team multiple Replace", 1, `<Replace>a</Replace><Replace>b</Replace>`, true, ""},
{"team Replace and non-Replace", 1, `<Replace>a</Replace><Get>b</Get>`, true, "Windows configuration profiles can only have <Replace> or <Add> top level elements."},
{
"team BitLocker profile", 1,
`<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/BitLocker/AllowStandardUserEncryption</LocURI></Target></Item></Replace>`, true,
syncml.DiskEncryptionProfileRestrictionErrMsg,
},
{"team Windows updates profile", 1, `<Replace><Item><Target><LocURI> ./Device/Vendor/MSFT/Policy/Config/Update/ConfigureDeadlineNoAutoRebootForFeatureUpdates </LocURI></Target></Item></Replace>`, true, "Custom configuration profiles can't include Windows updates settings."},
{"invalid team", 2, `<Replace></Replace>`, true, "not found"},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: c.mdmConfigured,
},
}, nil
}
ctx = test.UserContext(ctx, test.UserAdmin)
_, err := svc.NewMDMWindowsConfigProfile(ctx, c.tmID, "foo", []byte(c.profile), nil, fleet.LabelsIncludeAll)
if c.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, c.wantErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestMDMBatchSetProfiles(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true})
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{
OrgName: "Foo Inc.",
},
ServerSettings: fleet.ServerSettings{
ServerURL: "https://foo.example.com",
},
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: true,
AndroidEnabledAndConfigured: true,
},
}, nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
return &fleet.Team{ID: 1, Name: name}, nil
}
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
return &fleet.Team{ID: id, Name: "team"}, nil
}
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, profVars []fleet.MDMProfileIdentifierFleetVariables,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string,
hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
return nil
}
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
return document, nil, nil
}
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
return &fleet.GroupedCertificateAuthorities{}, nil
}
testCases := []struct {
name string
user *fleet.User
premium bool
teamID *uint
teamName *string
profiles []fleet.MDMProfileBatchPayload
wantErr string
}{
{
"global admin",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
nil,
"",
},
{
"global admin, team",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
true,
ptr.Uint(1),
nil,
nil,
"",
},
{
"global maintainer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
false,
nil,
nil,
nil,
"",
},
{
"global maintainer, team",
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
true,
ptr.Uint(1),
nil,
nil,
"",
},
{
"global observer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
false,
nil,
nil,
nil,
authz.ForbiddenErrorMessage,
},
{
"team admin, DOES belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
true,
ptr.Uint(1),
nil,
nil,
"",
},
{
"team admin, DOES belong to team by name",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
true,
nil,
ptr.String("team"),
nil,
"",
},
{
"team admin, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
true,
ptr.Uint(1),
nil,
nil,
authz.ForbiddenErrorMessage,
},
{
"team admin, DOES NOT belong to team by name",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
true,
nil,
ptr.String("team"),
nil,
authz.ForbiddenErrorMessage,
},
{
"team maintainer, DOES belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
true,
ptr.Uint(1),
nil,
nil,
"",
},
{
"team maintainer, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
true,
ptr.Uint(1),
nil,
nil,
authz.ForbiddenErrorMessage,
},
{
"team observer, DOES belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
true,
ptr.Uint(1),
nil,
nil,
authz.ForbiddenErrorMessage,
},
{
"team observer, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
true,
ptr.Uint(1),
nil,
nil,
authz.ForbiddenErrorMessage,
},
{
"user no roles",
&fleet.User{ID: 1337},
false,
nil,
nil,
nil,
authz.ForbiddenErrorMessage,
},
{
"team id with free license",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
ptr.Uint(1),
nil,
nil,
ErrMissingLicense.Error(),
},
{
"team name with free license",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
ptr.String("team"),
nil,
ErrMissingLicense.Error(),
},
{
"team id and name specified",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
true,
ptr.Uint(1),
ptr.String("team"),
nil,
"cannot specify both team_id and team_name",
},
{
"duplicate macOS profile name",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
true,
ptr.Uint(1),
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: "N2", Contents: mobileconfigForTest("N1", "I2")},
},
`More than one configuration profile have the same name (PayloadDisplayName): "N1"`,
},
{
"duplicate macOS profile identifier",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
true,
ptr.Uint(1),
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: "N2", Contents: mobileconfigForTest("N2", "I2")},
{Name: "N3", Contents: mobileconfigForTest("N3", "I1")},
},
`More than one configuration profile have the same identifier (PayloadIdentifier): "I1"`,
},
{
"only macOS",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
true,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: "N2", Contents: mobileconfigForTest("N2", "I2")},
{Name: "N3", Contents: mobileconfigForTest("N3", "I3 $FLEET_VAR_HOST_END_USER_EMAIL_IDP")},
{Name: "N4", Contents: declBytesForTest("D1", "d1content")},
},
``,
},
{
"mixed profiles",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: syncMLForTest("./foo/bar")},
{Name: "N2", Contents: syncMLForTest("./baz")},
{Name: "N3", Contents: syncMLForTest("./zab")},
{Name: "N4", Contents: mobileconfigForTest("N4", "I1")},
{Name: "N5", Contents: mobileconfigForTest("N5", "I2")},
{Name: "N6", Contents: mobileconfigForTest("N6", "I3")},
{Name: "N7", Contents: androidConfigProfileForTest(t, "A1", nil).RawJSON},
{Name: "N8", Contents: androidConfigProfileForTest(t, "A2", nil).RawJSON},
},
``,
},
{
"only windows",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: syncMLForTest("./foo/bar")},
{Name: "N2", Contents: syncMLForTest("./baz")},
{Name: "N3", Contents: syncMLForTest("./zab")},
},
``,
},
{
"unsupported payload type",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{
Name: "foo", Contents: []byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>Enable</key>
<string>On</string>
<key>PayloadDisplayName</key>
<string>FileVault 2</string>
<key>PayloadIdentifier</key>
<string>com.apple.MCX.FileVault2.A5874654-D6BA-4649-84B5-43847953B369</string>
<key>PayloadType</key>
<string>%s</string>
<key>PayloadUUID</key>
<string>A5874654-D6BA-4649-84B5-43847953B369</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>Config Profile Name</string>
<key>PayloadIdentifier</key>
<string>com.example.config.FE42D0A2-DBA9-4B72-BC67-9288665B8D59</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>FE42D0A2-DBA9-4B72-BC67-9288665B8D59</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>`, mobileconfig.FleetFileVaultPayloadType)),
},
},
mobileconfig.DiskEncryptionProfileRestrictionErrMsg,
},
{
"unsupported Apple config profile Fleet variable",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N4", Contents: mobileconfigForTest("N4", "I${FLEET_VAR_BOZO}1")},
},
"Fleet variable",
},
{
"unsupported Apple declaration Fleet variable",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N4", Contents: declBytesForTest("D1", "d1content ${FLEET_VAR_BOZO}")},
},
"Fleet variable",
},
{
"unsupported Windows Fleet variable",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: syncMLForTest("./foo/$FLEET_VAR_BOZO/bar")},
},
"Fleet variable",
},
{
"fleet variable in android config is ignored",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: androidConfigProfileForTest(t, "$FLEET_VAR_BOZO", nil).RawJSON},
},
"",
},
{
"fleet variable in android config is ignored",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: androidConfigProfileForTest(t, "$FLEET_VAR_BOZO", nil).RawJSON},
},
"",
},
{
"duplicate android config profile names",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: androidConfigProfileForTest(t, "A1", nil).RawJSON},
{Name: "N1", Contents: androidConfigProfileForTest(t, "A2", nil).RawJSON},
},
"duplicate json by name",
},
{
"premium-only android profile without premium license",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "systemUpdate", Contents: json.RawMessage([]byte(`{"systemUpdate": {"type": "AUTOMATIC"}}`))},
},
`Android OS updates ("systemUpdate") is Fleet Premium only.`,
},
{
"premium-only android profile with premium license",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
true,
nil,
nil,
[]fleet.MDMProfileBatchPayload{
{Name: "systemUpdate", Contents: json.RawMessage([]byte(`{"systemUpdate": {"type": "AUTOMATIC"}}`))},
},
"",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
defer func() { ds.BatchSetMDMProfilesFuncInvoked = false }()
// prepare the context with the user and license
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
tier := fleet.TierFree
if tt.premium {
tier = fleet.TierPremium
}
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: tier})
err := svc.BatchSetMDMProfiles(ctx, tt.teamID, tt.teamName, tt.profiles, false, false, nil, false)
if tt.wantErr == "" {
require.NoError(t, err)
require.True(t, ds.BatchSetMDMProfilesFuncInvoked)
return
}
require.Error(t, err)
require.ErrorContains(t, err, tt.wantErr)
require.False(t, ds.BatchSetMDMProfilesFuncInvoked)
})
}
}
func TestValidateProfiles(t *testing.T) {
tests := []struct {
name string
profiles []fleet.MDMProfileBatchPayload
wantErr bool
errMsg string
}{
{
name: "Valid Darwin Profile",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "darwinProfile", Contents: []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")},
},
wantErr: false,
},
{
name: "Valid Windows Profile",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "windowsProfile", Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
},
wantErr: false,
},
{
name: "Valid Android Profile",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "androidProfile", Contents: androidConfigProfileForTest(t, "Profile1", nil).RawJSON},
},
wantErr: false,
},
{
name: "Invalid Profile",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "invalidProfile", Contents: []byte("invalid data")},
},
wantErr: true,
},
{
name: "Mixed Valid and Invalid Profiles",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "validProfile", Contents: []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")},
{Name: "invalidProfile", Contents: []byte("invalid data")},
},
wantErr: true,
},
{
name: "Empty Profile",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "emptyProfile", Contents: []byte("")},
},
wantErr: true,
},
{
name: "Windows Profile With Deprecated Labels",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "windowsProfile", Labels: []string{"a"}, Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
},
wantErr: false,
},
{
name: "Windows Profile With Excluded Labels",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "windowsProfile", LabelsExcludeAny: []string{"a"}, Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
},
wantErr: false,
},
{
name: "Windows Profile With Included Labels",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "windowsProfile", LabelsIncludeAll: []string{"a"}, Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
},
wantErr: false,
},
{
name: "Windows Profile With Mixed Labels",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "windowsProfile", Labels: []string{"z"}, LabelsIncludeAll: []string{"a"}, Contents: []byte("<replace><Target><LocURI>Custom/URI</LocURI></Target></replace>")},
},
wantErr: true,
},
{
name: "Too large profile",
profiles: []fleet.MDMProfileBatchPayload{
{Name: "hugeprofile", Contents: []byte(strings.Repeat("a", 1024*1024+1))},
},
wantErr: true,
errMsg: "validation failed: mdm maximum configuration profile file size is 1 MB",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Convert slice to a map
profiles := make(map[int]fleet.MDMProfileBatchPayload, len(tt.profiles))
for i, profile := range tt.profiles {
profiles[i] = profile
}
err := validateProfiles(profiles)
if tt.wantErr {
require.Error(t, err)
if tt.errMsg != "" {
require.Equal(t, tt.errMsg, err.Error())
}
} else {
require.NoError(t, err)
}
})
}
}
func TestBackwardsCompatProfilesParamUnmarshalJSON(t *testing.T) {
tests := []struct {
name string
input []byte
expect backwardsCompatProfilesParam
expectError bool
}{
{
name: "empty input",
input: []byte(""),
expect: nil,
expectError: false,
},
{
name: "new format",
input: []byte(`[{"name": "profile1", "contents": "Zm9vCg=="}, {"name": "profile2", "contents": "YmFyCg=="}]`),
expect: backwardsCompatProfilesParam{
{Name: "profile1", Contents: []byte("foo\n")},
{Name: "profile2", Contents: []byte("bar\n")},
},
expectError: false,
},
{
name: "new format with labels",
input: []byte(`[{"name": "profile1", "contents": "Zm9vCg==", "labels": ["foo", "bar"]}, {"name": "profile2", "contents": "YmFyCg=="}]`),
expect: backwardsCompatProfilesParam{
{Name: "profile1", Contents: []byte("foo\n"), Labels: []string{"foo", "bar"}},
{Name: "profile2", Contents: []byte("bar\n")},
},
expectError: false,
},
{
name: "old format",
input: []byte(`{"profile1": "Zm9vCg==", "profile2": "YmFyCg=="}`),
expect: backwardsCompatProfilesParam{
{Name: "profile1", Contents: []byte("foo\n")},
{Name: "profile2", Contents: []byte("bar\n")},
},
expectError: false,
},
{
name: "invalid json",
input: []byte(`{invalid json}`),
expect: nil,
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var bcp backwardsCompatProfilesParam
err := bcp.UnmarshalJSON(tc.input)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.ElementsMatch(t, tc.expect, bcp)
}
})
}
}
func TestMDMResendConfigProfileAuthz(t *testing.T) {
ds := new(mock.Store)
// while the config profiles are not premium-only, teams are and we want to test with teams.
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
testCases := []struct {
name string
user *fleet.User
shouldFailGlobalRead bool
shouldFailTeamRead bool
shouldFailGlobalWrite bool // this write action includes batch resend to multiple hosts
shouldFailTeamWrite bool // this write action includes batch resend to multiple hosts
shouldFailGlobalResendToSingleHost bool
shouldFailTeamResendToSingleHost bool
}{
{
"global admin",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
false,
false,
false,
false,
false,
},
{
"global maintainer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
false,
false,
false,
false,
false,
false,
},
{
"global observer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
true,
true,
true,
true,
true,
true,
},
{
"global observer+",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)},
true,
true,
true,
true,
true,
true,
},
{
"global gitops",
&fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)},
false,
false,
false,
false,
// GitOps doesn't really need permissions to resend to specific hosts,
// but we will keep this as-is to not break any workflows that might be using a
// GitOps token to do a resend.
false,
false,
},
{
"team admin, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
true,
false,
true,
false,
true,
false,
},
{
"team admin, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
true,
true,
true,
true,
true,
true,
},
{
"team maintainer, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
true,
false,
true,
false,
true,
false,
},
{
"team maintainer, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
true,
true,
true,
true,
true,
true,
},
{
"team observer, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
true,
true,
true,
true,
true,
true,
},
{
"team observer, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
true,
true,
true,
true,
true,
true,
},
{
"team observer+, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}},
true,
true,
true,
true,
true,
true,
},
{
"team observer+, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserverPlus}}},
true,
true,
true,
true,
true,
true,
},
{
"team gitops, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
true,
false,
true,
false,
true,
// GitOps doesn't really need permissions to resend to specific hosts,
// but we will keep this as-is to not break any workflows that might be using a
// GitOps token to do a resend.
false,
},
{
"team gitops, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleGitOps}}},
true,
true,
true,
true,
true,
true,
},
{
"user no roles",
&fleet.User{ID: 1337},
true,
true,
true,
true,
true,
true,
},
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: true,
},
}, nil
}
ds.HostLiteFunc = func(ctx context.Context, hid uint) (*fleet.Host, error) {
if hid == 1 {
return &fleet.Host{ID: hid, UUID: "host-uuid-1", Platform: "darwin", TeamID: ptr.Uint(1)}, nil
} else if hid == 1337 {
return &fleet.Host{ID: hid, UUID: "host-uuid-no-team", Platform: "darwin", TeamID: nil}, nil
}
return nil, &notFoundErr{}
}
ds.GetMDMAppleConfigProfileFunc = func(ctx context.Context, pid string) (*fleet.MDMAppleConfigProfile, error) {
var tid uint
if pid == "a-team-1-profile" {
tid = 1
}
return &fleet.MDMAppleConfigProfile{
ProfileUUID: pid,
TeamID: &tid,
}, nil
}
ds.GetHostMDMProfileInstallStatusFunc = func(ctx context.Context, hostUUID string, profUUID string) (fleet.MDMDeliveryStatus, error) {
return fleet.MDMDeliveryFailed, nil
}
ds.ResendHostMDMProfileFunc = func(ctx context.Context, hostUUID, profUUID string) error {
return nil
}
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
return nil
}
ds.BatchResendMDMProfileToHostsFunc = func(ctx context.Context, profUUID string, filters fleet.BatchResendMDMProfileFilters) (int64, error) {
return 0, nil
}
checkShouldFail := func(t *testing.T, err error, shouldFail bool) {
if !shouldFail {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
}
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
// ds.TeamWithExtrasFunc = mockTeamFuncWithUser(tt.user)
// test authz resend config profile (no team)
err := svc.ResendHostMDMProfile(ctx, 1337, "a-no-team-profile")
checkShouldFail(t, err, tt.shouldFailGlobalResendToSingleHost)
err = svc.BatchResendMDMProfileToHosts(ctx, "a-no-team-profile", fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
checkShouldFail(t, err, tt.shouldFailGlobalWrite)
// test authz resend config profile (team 1)
err = svc.ResendHostMDMProfile(ctx, 1, "a-team-1-profile")
checkShouldFail(t, err, tt.shouldFailTeamResendToSingleHost)
err = svc.BatchResendMDMProfileToHosts(ctx, "a-team-1-profile", fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
checkShouldFail(t, err, tt.shouldFailTeamWrite)
})
}
}
func TestBatchSetMDMProfilesLabels(t *testing.T) {
ds := new(mock.Store)
// while the config profiles are not premium-only, teams are and we want to test with teams.
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
_ = ctx
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: true,
AndroidEnabledAndConfigured: true,
},
}, nil
}
ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
return &fleet.Team{
ID: tid,
Name: "team1",
}, nil
}
type ProfileLabels struct {
IncludeAll bool
IncludeAny bool
ExcludeAny bool
}
profileLabels := map[string]*ProfileLabels{}
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, profVars []fleet.MDMProfileIdentifierFleetVariables) (updates fleet.MDMProfilesUpdates, err error) {
for _, profile := range macProfiles {
profileLabels[profile.Name] = &ProfileLabels{}
if len(profile.LabelsIncludeAll) > 0 {
assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name)
assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
profileLabels[profile.Name].IncludeAll = true
}
if len(profile.LabelsIncludeAny) > 0 {
assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
profileLabels[profile.Name].IncludeAny = true
}
if len(profile.LabelsExcludeAny) > 0 {
assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name)
profileLabels[profile.Name].ExcludeAny = true
}
}
for _, profile := range winProfiles {
profileLabels[profile.Name] = &ProfileLabels{}
if len(profile.LabelsIncludeAll) > 0 {
assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name)
assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
profileLabels[profile.Name].IncludeAll = true
}
if len(profile.LabelsIncludeAny) > 0 {
assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
profileLabels[profile.Name].IncludeAny = true
}
if len(profile.LabelsExcludeAny) > 0 {
assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name)
profileLabels[profile.Name].ExcludeAny = true
}
}
for _, profile := range macDeclarations {
profileLabels[profile.Name] = &ProfileLabels{}
if len(profile.LabelsIncludeAll) > 0 {
assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name)
assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
profileLabels[profile.Name].IncludeAll = true
}
if len(profile.LabelsIncludeAny) > 0 {
assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
profileLabels[profile.Name].IncludeAny = true
}
if len(profile.LabelsExcludeAny) > 0 {
assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name)
profileLabels[profile.Name].ExcludeAny = true
}
}
for _, profile := range androidProfiles {
profileLabels[profile.Name] = &ProfileLabels{}
if len(profile.LabelsIncludeAll) > 0 {
assert.True(t, profile.LabelsIncludeAll[0].RequireAll, "profile label missing RequireAll: %s", profile.Name)
assert.False(t, profile.LabelsIncludeAll[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
profileLabels[profile.Name].IncludeAll = true
}
if len(profile.LabelsIncludeAny) > 0 {
assert.False(t, profile.LabelsIncludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
assert.False(t, profile.LabelsIncludeAny[0].Exclude, "profile label shouldn't have Exclude: %s", profile.Name)
profileLabels[profile.Name].IncludeAny = true
}
if len(profile.LabelsExcludeAny) > 0 {
assert.False(t, profile.LabelsExcludeAny[0].RequireAll, "profile label shouldn't have RequireAll: %s", profile.Name)
assert.True(t, profile.LabelsExcludeAny[0].Exclude, "profile label should have Exclude: %s", profile.Name)
profileLabels[profile.Name].ExcludeAny = true
}
}
return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
var labelID uint
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
m := map[string]uint{}
for _, label := range names {
if label != "baddy" {
labelID++
m[label] = labelID
}
}
return m, nil
}
ds.LabelsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]*fleet.Label, error) {
m := map[string]*fleet.Label{}
for _, name := range names {
if name != "baddy" {
labelID++
m[name] = &fleet.Label{
ID: labelID,
Name: name,
}
}
}
return m, nil
}
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
return nil
}
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
return document, nil, nil
}
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
return &fleet.GroupedCertificateAuthorities{}, nil
}
profiles := []fleet.MDMProfileBatchPayload{
// macOS
{
Name: "MIncAll",
Contents: mobileconfigForTest("MIncAll", "1"),
LabelsIncludeAll: []string{"a", "b"},
},
{
Name: "MIncAny",
Contents: mobileconfigForTest("MIncAny", "2"),
LabelsIncludeAny: []string{"a", "b"},
},
{
Name: "MExclAny",
Contents: mobileconfigForTest("MExclAny", "3"),
LabelsExcludeAny: []string{"a", "b"},
},
// Windows
{
Name: "WIncAll",
Contents: syncMLForTest("./Foo/Bar"),
LabelsIncludeAll: []string{"a", "b"},
},
{
Name: "WIncAny",
Contents: syncMLForTest("./Foo/Barz"),
LabelsIncludeAny: []string{"a", "b"},
},
{
Name: "WExclAny",
Contents: syncMLForTest("./Foo/Barf"),
LabelsExcludeAny: []string{"a", "b"},
},
// Declarative
{
Name: "DIncAll",
Contents: declarationForTest("DIncAll"),
LabelsIncludeAll: []string{"a", "b"},
},
{
Name: "DIncAny",
Contents: declarationForTest("DIncAny"),
LabelsIncludeAny: []string{"a", "b"},
},
{
Name: "DExclAny",
Contents: declarationForTest("DExclAny"),
LabelsExcludeAny: []string{"a", "b"},
},
// Android
{
Name: "AIncAll",
Contents: androidConfigProfileForTest(t, "AIncAll", nil).RawJSON,
LabelsIncludeAll: []string{"a", "b"},
},
{
Name: "AIncAny",
Contents: androidConfigProfileForTest(t, "AIncAny", nil).RawJSON,
LabelsIncludeAny: []string{"a", "b"},
},
{
Name: "AExclAny",
Contents: androidConfigProfileForTest(t, "AExclAny", nil).RawJSON,
LabelsExcludeAny: []string{"a", "b"},
},
}
authCtx := test.UserContext(ctx, test.UserAdmin)
err := svc.BatchSetMDMProfiles(authCtx, ptr.Uint(1), nil, profiles, false, false, ptr.Bool(true), false)
require.NoError(t, err)
assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["MIncAll"])
assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["MIncAny"])
assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["MExclAny"])
assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["WIncAll"])
assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["WIncAny"])
assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["WExclAny"])
assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["DIncAll"])
assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["DIncAny"])
assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["DExclAny"])
assert.Equal(t, ProfileLabels{IncludeAll: true}, *profileLabels["AIncAll"])
assert.Equal(t, ProfileLabels{IncludeAny: true}, *profileLabels["AIncAny"])
assert.Equal(t, ProfileLabels{ExcludeAny: true}, *profileLabels["AExclAny"])
// Test that a bad label doesn't pass validation...
err = svc.BatchSetMDMProfiles(authCtx, ptr.Uint(1), nil, []fleet.MDMProfileBatchPayload{{
Name: "Baddy",
Contents: declarationForTest("Baddy"),
LabelsExcludeAny: []string{"baddy"},
}}, false, false, ptr.Bool(true), false)
require.Error(t, err)
require.ErrorContains(t, err, `Label "baddy" doesn't exist. Please remove the label from the configuration profile.`)
// ...unless we're in dry run mode
err = svc.BatchSetMDMProfiles(authCtx, ptr.Uint(1), nil, []fleet.MDMProfileBatchPayload{{
Name: "Baddy",
Contents: declarationForTest("Baddy"),
LabelsExcludeAny: []string{"baddy"},
}}, true, false, ptr.Bool(true), false)
require.NoError(t, err)
}
func androidConfigProfileForTest(t *testing.T, name string, content map[string]any, labels ...*fleet.Label) *fleet.MDMAndroidConfigProfile {
if content == nil {
content = make(map[string]any)
}
content["name"] = name
rawJSON, err := json.Marshal(content)
require.NoError(t, err)
prof := &fleet.MDMAndroidConfigProfile{
Name: name,
RawJSON: rawJSON,
}
for _, lbl := range labels {
switch {
case strings.HasPrefix(lbl.Name, "exclude-"):
prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
case strings.HasPrefix(lbl.Name, "include-any-"):
prof.LabelsIncludeAny = append(prof.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
default:
prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
}
}
return prof
}
func TestUploadMDMAppleAPNSCertReplacesFileVaultProfile(t *testing.T) {
// We want to verify here that the disk encryption profile get's deleted for apple.
ds := new(mock.Store)
lic := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{SkipCreateTestUsers: true, License: lic})
ctx = test.UserContext(ctx, test.UserAdmin)
ctx = license.NewContext(ctx, lic)
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
require.NoError(t, err)
crt, key, err := apple_mdm.NewSCEPCACertKey()
require.NoError(t, err)
scepCert := tokenpki.PEMCertificate(crt.Raw)
scepKey := tokenpki.PEMRSAPrivateKey(key)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
_ sqlx.QueryerContext,
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Value: scepCert},
fleet.MDMAssetCAKey: {Value: scepKey},
fleet.MDMAssetAPNSKey: {Value: apnsKey},
fleet.MDMAssetAPNSCert: {Value: apnsCert},
}, nil
}
ds.DeleteMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) error {
require.Contains(t, assetNames, fleet.MDMAssetAPNSCert)
return nil
}
ds.InsertMDMConfigAssetsFunc = func(ctx context.Context, assets []fleet.MDMConfigAsset, tx sqlx.ExtContext) error {
return nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: false,
EnableDiskEncryption: optjson.SetBool(true),
},
}, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) error {
require.True(t, info.MDM.EnabledAndConfigured)
return nil
}
newActivityCalls := 0
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
act := fleet.ActivityTypeEnabledMacosDiskEncryption{}
require.Equal(t, act.ActivityName(), activity.ActivityName())
newActivityCalls++
return nil
}
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
return []*fleet.TeamSummary{
{ID: 1, Name: "Team1"},
{ID: 2, Name: "Team2"},
}, nil
}
ds.GetConfigEnableDiskEncryptionFunc = func(ctx context.Context, teamID *uint) (fleet.DiskEncryptionConfig, error) {
if *teamID == 1 {
return fleet.DiskEncryptionConfig{Enabled: true}, nil
}
return fleet.DiskEncryptionConfig{Enabled: false}, nil
}
deleteCalls := uint(0)
ds.DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc = func(ctx context.Context, teamID *uint, profileIdentifier string) error {
require.Equal(t, mobileconfig.FleetFileVaultPayloadIdentifier, profileIdentifier)
if deleteCalls == 0 {
// No Team
require.Nil(t, teamID)
} else {
require.NotNil(t, teamID)
require.Equal(t, deleteCalls, *teamID)
}
deleteCalls++
return nil
}
newProfileCalls := uint(0)
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, p fleet.MDMAppleConfigProfile, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
require.Nil(t, usesFleetVars) // Filevault does not use fleet vars
require.Equal(t, mobileconfig.FleetFileVaultPayloadIdentifier, p.Identifier)
if newProfileCalls == 0 {
// No Team
require.Nil(t, p.TeamID)
} else {
require.NotNil(t, p.TeamID)
require.Equal(t, newProfileCalls, *p.TeamID)
}
newProfileCalls++
return nil, nil
}
err = svc.UploadMDMAppleAPNSCert(ctx, bytes.NewReader(apnsCert))
require.NoError(t, err)
require.EqualValues(t, 2, newProfileCalls)
require.EqualValues(t, 2, deleteCalls)
require.EqualValues(t, 2, newActivityCalls) // Only enabled Disk encryption activities, we don't want to log disable right before enabling.
}
func TestNewMDMProfilePremiumOnlyAndroid(t *testing.T) {
require.Len(t, fleet.AndroidPremiumOnlyJSONKeys, 1, "update this test with any new premium-only key for android profiles")
require.Contains(t, fleet.AndroidPremiumOnlyJSONKeys, "systemUpdate", "update this test with any new premium-only key for android profiles")
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true})
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{
OrgName: "Foo Inc.",
},
ServerSettings: fleet.ServerSettings{
ServerURL: "https://foo.example.com",
},
MDM: fleet.MDM{
EnabledAndConfigured: true,
WindowsEnabledAndConfigured: true,
AndroidEnabledAndConfigured: true,
},
}, nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
return &fleet.Team{ID: 1, Name: name}, nil
}
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
return &fleet.Team{ID: id, Name: "team"}, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
return nil
}
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
return nil
}
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
return document, nil, nil
}
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
return &fleet.GroupedCertificateAuthorities{}, nil
}
ds.NewMDMAndroidConfigProfileFunc = func(ctx context.Context, cp fleet.MDMAndroidConfigProfile) (*fleet.MDMAndroidConfigProfile, error) {
return &fleet.MDMAndroidConfigProfile{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
testCases := []struct {
name string
user *fleet.User
premium bool
teamID uint
profile string
wantErr string
}{
{
"premium-only android profile without premium license",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
0,
`{"systemUpdate": {"type": "AUTOMATIC"}}`,
`Android OS updates ("systemUpdate") is Fleet Premium only.`,
},
{
"premium-only android profile with premium license",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
true,
0,
`{"systemUpdate": {"type": "AUTOMATIC"}}`,
"",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
defer func() { ds.NewMDMAndroidConfigProfileFuncInvoked = false }()
// prepare the context with the user and license
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
tier := fleet.TierFree
if tt.premium {
tier = fleet.TierPremium
}
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: tier})
_, err := svc.NewMDMAndroidConfigProfile(ctx, tt.teamID, tt.name, []byte(tt.profile), nil, fleet.LabelsIncludeAll)
if tt.wantErr == "" {
require.NoError(t, err)
require.True(t, ds.NewMDMAndroidConfigProfileFuncInvoked)
return
}
require.Error(t, err)
require.ErrorContains(t, err, tt.wantErr)
require.False(t, ds.NewMDMAndroidConfigProfileFuncInvoked)
})
}
}