SAAD: Asset CRUD API (#49011)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #48568 partly # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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/guides/committing-changes.md#changes-files) for more information. (Will add in followup) - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Apple DDM asset management endpoints: list, get, download (raw JSON), create, and delete. * Implemented datastore-backed Apple DDM asset CRUD with team-scoped and global access, plus configurable upload size limits. * Added strict asset JSON validation (including required fields, URI checks, and secret expansion rules). * **Bug Fixes** * Improved authorization handling by returning not-found responses for out-of-scope read/download/delete to avoid asset discovery. * Added clearer conflict and linked-profile error mapping for create/delete failures. * **Tests** * Added comprehensive authorization and validation test coverage for Apple DDM assets and policy behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -2,12 +2,16 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
mdmcrypto "github.com/fleetdm/fleet/v4/server/mdm/crypto"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
)
|
||||
|
||||
func (svc *Service) GetMDMAccountDrivenEnrollmentSSOURL(ctx context.Context, enrollmentToken string) (string, error) {
|
||||
@@ -81,3 +85,147 @@ func (svc *Service) GetMDMAppleAccountEnrollmentProfile(ctx context.Context, enr
|
||||
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ListAppleDDMAssets(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.DDMAssetAuthz{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assets, err := svc.ds.ListAppleDDMAssets(ctx, teamID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "listing Apple DDM assets")
|
||||
}
|
||||
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetAppleDDMAsset(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
if authzErr := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); authzErr != nil {
|
||||
return nil, authzErr
|
||||
}
|
||||
|
||||
asset, err := svc.ds.GetAppleDDMAsset(ctx, assetUUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting Apple DDM asset")
|
||||
}
|
||||
|
||||
if authzErr := svc.authz.Authorize(ctx, &fleet.DDMAssetAuthz{TeamID: asset.TeamID}, fleet.ActionRead); authzErr != nil {
|
||||
// We return a not found error here to avoid leaking the existence of the asset to unauthorized users.
|
||||
return nil, common_mysql.NotFound("Asset").WithName(assetUUID)
|
||||
}
|
||||
|
||||
return asset, nil
|
||||
}
|
||||
|
||||
func (svc *Service) DownloadAppleDDMAsset(ctx context.Context, assetUUID string) (name string, data []byte, err error) {
|
||||
if authzErr := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); authzErr != nil {
|
||||
return "", nil, authzErr
|
||||
}
|
||||
|
||||
asset, err := svc.ds.GetAppleDDMAssetForDownload(ctx, assetUUID)
|
||||
if err != nil {
|
||||
return "", nil, ctxerr.Wrap(ctx, err, "getting Apple DDM asset")
|
||||
}
|
||||
|
||||
if authzErr := svc.authz.Authorize(ctx, &fleet.DDMAssetAuthz{TeamID: asset.TeamID}, fleet.ActionRead); authzErr != nil {
|
||||
// We return a not found error here to avoid leaking the existence of the asset to unauthorized users.
|
||||
return "", nil, common_mysql.NotFound("Asset").WithName(assetUUID)
|
||||
}
|
||||
|
||||
return asset.Name + ".json", asset.Data, nil
|
||||
}
|
||||
|
||||
func (svc *Service) CreateAppleDDMAsset(ctx context.Context, teamID *uint, name string, data []byte) (string, error) {
|
||||
if authzErr := svc.authz.Authorize(ctx, &fleet.DDMAssetAuthz{TeamID: teamID}, fleet.ActionWrite); authzErr != nil {
|
||||
return "", authzErr
|
||||
}
|
||||
|
||||
identifier, err := svc.validateAppleDDMAsset(ctx, data)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "validating Apple DDM asset")
|
||||
}
|
||||
|
||||
assetUUID, err := svc.ds.CreateAppleDDMAsset(ctx, name, identifier, data, teamID)
|
||||
if err != nil {
|
||||
if alreadyExistsErr, ok := err.(fleet.AlreadyExistsError); ok && alreadyExistsErr.IsExists() {
|
||||
switch {
|
||||
case strings.Contains(alreadyExistsErr.Error(), "asset_name"):
|
||||
return "", &fleet.ConflictError{Message: fmt.Sprintf("An asset with the name %q already exists for this team", name)}
|
||||
case strings.Contains(alreadyExistsErr.Error(), "asset_identifier"):
|
||||
return "", &fleet.ConflictError{Message: fmt.Sprintf("An asset with the identifier %q already exists for this team", identifier)}
|
||||
}
|
||||
}
|
||||
return "", ctxerr.Wrap(ctx, err, "creating Apple DDM asset")
|
||||
}
|
||||
|
||||
return assetUUID, nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateAppleDDMAsset(ctx context.Context, data []byte) (identifier string, err error) {
|
||||
var rawAsset fleet.RawDDMAsset
|
||||
if err := json.Unmarshal(data, &rawAsset); err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "unmarshaling asset data")
|
||||
}
|
||||
|
||||
if rawAsset.Identifier == "" {
|
||||
return "", &fleet.BadRequestError{Message: "Asset must contain a non-empty identifier"}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(rawAsset.Type, "com.apple.asset.") {
|
||||
return "", &fleet.BadRequestError{Message: "Asset type must be a valid Apple asset type beginning with 'com.apple.asset.'"}
|
||||
}
|
||||
|
||||
// Check if Identifier uses a FLEET_SECRET, fail if so.
|
||||
if strings.Contains(rawAsset.Identifier, "FLEET_SECRET") {
|
||||
return "", &fleet.BadRequestError{Message: "Asset identifier must not contain a $FLEET_SECRET"}
|
||||
}
|
||||
|
||||
expanded, _, err := svc.ds.ExpandEmbeddedSecretsAndUpdatedAt(ctx, string(data))
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "expanding embedded secrets and updated_at")
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(expanded), &rawAsset); err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "unmarshaling asset data")
|
||||
}
|
||||
|
||||
// We disallow authentication, as we force MDM auth when serving the assets.
|
||||
if rawAsset.Payload.Authentication != nil {
|
||||
return "", &fleet.BadRequestError{Message: "Asset payload must not contain an authentication key"}
|
||||
}
|
||||
|
||||
if rawAsset.Payload.Reference.DataURL == "" {
|
||||
return "", &fleet.BadRequestError{Message: "Asset payload must contain a non-empty reference data URL"}
|
||||
}
|
||||
|
||||
if _, err := url.ParseRequestURI(rawAsset.Payload.Reference.DataURL); err != nil {
|
||||
return "", &fleet.BadRequestError{Message: fmt.Sprintf("Invalid payload data URL: %v", err)}
|
||||
}
|
||||
|
||||
return rawAsset.Identifier, nil
|
||||
}
|
||||
|
||||
func (svc *Service) DeleteAppleDDMAsset(ctx context.Context, assetUUID string) error {
|
||||
if authzErr := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); authzErr != nil {
|
||||
return authzErr
|
||||
}
|
||||
|
||||
asset, err := svc.ds.GetAppleDDMAsset(ctx, assetUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting Apple DDM asset")
|
||||
}
|
||||
|
||||
if authzErr := svc.authz.Authorize(ctx, &fleet.DDMAssetAuthz{TeamID: asset.TeamID}, fleet.ActionWrite); authzErr != nil {
|
||||
// We return a not found error here to avoid leaking the existence of the asset to unauthorized users.
|
||||
return common_mysql.NotFound("Asset").WithName(assetUUID)
|
||||
}
|
||||
|
||||
if err := svc.ds.DeleteAppleDDMAsset(ctx, assetUUID); err != nil {
|
||||
if fleet.IsForeignKey(err) {
|
||||
return &fleet.BadRequestError{Message: "Couldn't delete. A configuration profile is linked to this asset. Please delete the profile and try again."}
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "deleting Apple DDM asset")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListAppleDDMAssets(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestService(t, ds)
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleAdmin)}})
|
||||
|
||||
t.Run("Observer cannot list DDM assets", func(t *testing.T) {
|
||||
ds.ListAppleDDMAssetsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error) {
|
||||
return []*fleet.DDMAsset{}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleObserver)}})
|
||||
|
||||
_, err := svc.ListAppleDDMAssets(ctx, nil)
|
||||
require.Error(t, err)
|
||||
var forbiddenErr *authz.Forbidden
|
||||
require.ErrorAs(t, err, &forbiddenErr)
|
||||
require.False(t, ds.ListAppleDDMAssetsFuncInvoked)
|
||||
|
||||
ds.ListAppleDDMAssetsFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Global admin can list DDM assets", func(t *testing.T) {
|
||||
ds.ListAppleDDMAssetsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error) {
|
||||
return []*fleet.DDMAsset{}, nil
|
||||
}
|
||||
|
||||
_, err := svc.ListAppleDDMAssets(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.ListAppleDDMAssetsFuncInvoked)
|
||||
|
||||
ds.ListAppleDDMAssetsFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin can list DDM assets for their team", func(t *testing.T) {
|
||||
ds.ListAppleDDMAssetsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error) {
|
||||
return []*fleet.DDMAsset{}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleAdmin), Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
|
||||
_, err := svc.ListAppleDDMAssets(ctx, new(uint(1)))
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.ListAppleDDMAssetsFuncInvoked)
|
||||
|
||||
ds.ListAppleDDMAssetsFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin cannot list DDM assets for other teams", func(t *testing.T) {
|
||||
ds.ListAppleDDMAssetsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error) {
|
||||
return []*fleet.DDMAsset{}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
|
||||
_, err := svc.ListAppleDDMAssets(ctx, new(uint(2)))
|
||||
require.Error(t, err)
|
||||
var forbiddenErr *authz.Forbidden
|
||||
require.ErrorAs(t, err, &forbiddenErr)
|
||||
require.False(t, ds.ListAppleDDMAssetsFuncInvoked)
|
||||
|
||||
ds.ListAppleDDMAssetsFuncInvoked = false
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAppleDDMAsset(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestService(t, ds)
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleAdmin)}})
|
||||
|
||||
t.Run("Observer cannot get DDM asset", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleObserver)}})
|
||||
_, err := svc.GetAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team Observer cannot get DDM asset", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}}})
|
||||
_, err := svc.GetAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Global admin can get DDM asset", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID}, nil
|
||||
}
|
||||
asset, err := svc.GetAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "some-asset-uuid", asset.AssetUUID)
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin can get DDM asset for their team", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID, TeamID: new(uint(1))}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
asset, err := svc.GetAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "some-asset-uuid", asset.AssetUUID)
|
||||
require.Equal(t, uint(1), *asset.TeamID)
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin cannot get DDM asset for other teams", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID, TeamID: new(uint(2))}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
_, err := svc.GetAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Not found asset returns not found error", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return nil, common_mysql.NotFound("asset")
|
||||
}
|
||||
|
||||
_, err := svc.GetAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloadAppleDDMAsset(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestService(t, ds)
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleAdmin)}})
|
||||
|
||||
t.Run("Observer cannot download DDM asset", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetForDownloadFunc = func(ctx context.Context, assetUUID string) (*fleet.DownloadableDDMAsset, error) {
|
||||
return &fleet.DownloadableDDMAsset{DDMAsset: fleet.DDMAsset{AssetUUID: assetUUID}, Data: []byte("some data")}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleObserver)}})
|
||||
_, _, err := svc.DownloadAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetForDownloadFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetForDownloadFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team Observer cannot download DDM asset", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetForDownloadFunc = func(ctx context.Context, assetUUID string) (*fleet.DownloadableDDMAsset, error) {
|
||||
return &fleet.DownloadableDDMAsset{DDMAsset: fleet.DDMAsset{AssetUUID: assetUUID}, Data: []byte("some data")}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}}})
|
||||
_, _, err := svc.DownloadAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetForDownloadFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetForDownloadFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Global admin can download DDM asset", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetForDownloadFunc = func(ctx context.Context, assetUUID string) (*fleet.DownloadableDDMAsset, error) {
|
||||
return &fleet.DownloadableDDMAsset{DDMAsset: fleet.DDMAsset{AssetUUID: assetUUID, Name: assetUUID}, Data: []byte("some data")}, nil
|
||||
}
|
||||
name, data, err := svc.DownloadAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "some-asset-uuid.json", name)
|
||||
require.Equal(t, []byte("some data"), data)
|
||||
require.True(t, ds.GetAppleDDMAssetForDownloadFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetForDownloadFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin can download DDM asset for their team", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetForDownloadFunc = func(ctx context.Context, assetUUID string) (*fleet.DownloadableDDMAsset, error) {
|
||||
return &fleet.DownloadableDDMAsset{DDMAsset: fleet.DDMAsset{AssetUUID: assetUUID, Name: assetUUID, TeamID: new(uint(1))}, Data: []byte("some data")}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
name, data, err := svc.DownloadAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "some-asset-uuid.json", name)
|
||||
require.Equal(t, []byte("some data"), data)
|
||||
require.True(t, ds.GetAppleDDMAssetForDownloadFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetForDownloadFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin cannot download DDM asset for other teams", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetForDownloadFunc = func(ctx context.Context, assetUUID string) (*fleet.DownloadableDDMAsset, error) {
|
||||
return &fleet.DownloadableDDMAsset{DDMAsset: fleet.DDMAsset{AssetUUID: assetUUID, TeamID: new(uint(2))}, Data: []byte("some data")}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
_, _, err := svc.DownloadAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetForDownloadFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetForDownloadFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Not found asset returns not found error", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetForDownloadFunc = func(ctx context.Context, assetUUID string) (*fleet.DownloadableDDMAsset, error) {
|
||||
return nil, common_mysql.NotFound("asset")
|
||||
}
|
||||
|
||||
_, _, err := svc.DownloadAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetForDownloadFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetForDownloadFuncInvoked = false
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteAppleDDMAsset(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestService(t, ds)
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleAdmin)}})
|
||||
|
||||
t.Run("Observer cannot delete DDM asset", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleObserver)}})
|
||||
err := svc.DeleteAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team Observer cannot delete DDM asset", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID}, nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}}})
|
||||
err := svc.DeleteAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Global admin can delete DDM asset", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID}, nil
|
||||
}
|
||||
ds.DeleteAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := svc.DeleteAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
require.True(t, ds.DeleteAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
ds.DeleteAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin can delete DDM asset for their team", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID, TeamID: new(uint(1))}, nil
|
||||
}
|
||||
ds.DeleteAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) error {
|
||||
return nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
|
||||
err := svc.DeleteAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
require.True(t, ds.DeleteAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
ds.DeleteAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin cannot delete DDM asset for other teams", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return &fleet.DDMAsset{AssetUUID: assetUUID, TeamID: new(uint(2))}, nil
|
||||
}
|
||||
ds.DeleteAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) error {
|
||||
return nil
|
||||
}
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
|
||||
err := svc.DeleteAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
require.False(t, ds.DeleteAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
ds.DeleteAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Not found asset returns not found error", func(t *testing.T) {
|
||||
ds.GetAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
return nil, common_mysql.NotFound("asset")
|
||||
}
|
||||
ds.DeleteAppleDDMAssetFunc = func(ctx context.Context, assetUUID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := svc.DeleteAppleDDMAsset(ctx, "some-asset-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.True(t, ds.GetAppleDDMAssetFuncInvoked)
|
||||
require.False(t, ds.DeleteAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.GetAppleDDMAssetFuncInvoked = false
|
||||
ds.DeleteAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateAppleDDMAsset(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestService(t, ds)
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleAdmin)}})
|
||||
|
||||
validData := []byte(`{"Type":"com.apple.asset.data","Identifier":"com.example.asset","Payload":{"Reference":{"DataURL":"https://example.com/data"}}}`)
|
||||
|
||||
ds.CreateAppleDDMAssetFunc = func(ctx context.Context, name, identifier string, data []byte, teamID *uint) (string, error) {
|
||||
return "some-asset-uuid", nil
|
||||
}
|
||||
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
||||
return document, nil, nil
|
||||
}
|
||||
|
||||
reset := func() {
|
||||
ds.CreateAppleDDMAssetFuncInvoked = false
|
||||
ds.ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked = false
|
||||
}
|
||||
|
||||
t.Run("Observer cannot create DDM asset", func(t *testing.T) {
|
||||
defer reset()
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: new(fleet.RoleObserver)}})
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", validData)
|
||||
require.Error(t, err)
|
||||
var forbiddenErr *authz.Forbidden
|
||||
require.ErrorAs(t, err, &forbiddenErr)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.CreateAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Global admin can create DDM asset", func(t *testing.T) {
|
||||
defer reset()
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", validData)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.CreateAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin can create DDM asset for their team", func(t *testing.T) {
|
||||
defer reset()
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, new(uint(1)), "asset", validData)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.CreateAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Team admin cannot create DDM asset for other teams", func(t *testing.T) {
|
||||
defer reset()
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: &fleet.User{GlobalRole: nil, Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}})
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, new(uint(2)), "asset", validData)
|
||||
require.Error(t, err)
|
||||
var forbiddenErr *authz.Forbidden
|
||||
require.ErrorAs(t, err, &forbiddenErr)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
|
||||
ds.CreateAppleDDMAssetFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("Malformed JSON is rejected", func(t *testing.T) {
|
||||
defer reset()
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", []byte(`{not json`))
|
||||
require.Error(t, err)
|
||||
require.False(t, ds.ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("Empty identifier is rejected", func(t *testing.T) {
|
||||
defer reset()
|
||||
data := []byte(`{"Type":"com.apple.asset.data","Identifier":"","Payload":{"Reference":{"DataURL":"https://example.com/data"}}}`)
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", data)
|
||||
require.Error(t, err)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("Invalid asset type is rejected", func(t *testing.T) {
|
||||
defer reset()
|
||||
data := []byte(`{"Type":"com.example.data","Identifier":"com.example.asset","Payload":{"Reference":{"DataURL":"https://example.com/data"}}}`)
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", data)
|
||||
require.Error(t, err)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("Empty payload reference data URL is rejected", func(t *testing.T) {
|
||||
defer reset()
|
||||
data := []byte(`{"Type":"com.apple.asset.data","Identifier":"com.example.asset","Payload":{"Reference":{"DataURL":""}}}`)
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", data)
|
||||
require.Error(t, err)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("Invalid payload reference data URL is rejected", func(t *testing.T) {
|
||||
defer reset()
|
||||
data := []byte(`{"Type":"com.apple.asset.data","Identifier":"com.example.asset","Payload":{"Reference":{"DataURL":"notaurl"}}}`)
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", data)
|
||||
require.Error(t, err)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("Secret in type is rejected before expansion", func(t *testing.T) {
|
||||
defer reset()
|
||||
data := []byte(`{"Type":"$FLEET_SECRET_TYPE","Identifier":"com.example.asset","Payload":{"Reference":{"DataURL":"https://example.com/data"}}}`)
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", data)
|
||||
require.Error(t, err)
|
||||
require.False(t, ds.ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("Secret in identifier is rejected before expansion", func(t *testing.T) {
|
||||
defer reset()
|
||||
data := []byte(`{"Type":"com.apple.asset.data","Identifier":"$FLEET_SECRET_ID","Payload":{"Reference":{"DataURL":"https://example.com/data"}}}`)
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", data)
|
||||
require.Error(t, err)
|
||||
require.False(t, ds.ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("Secret in payload data URL is expanded and allowed", func(t *testing.T) {
|
||||
defer reset()
|
||||
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
||||
return string(validData), nil, nil
|
||||
}
|
||||
defer func() {
|
||||
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
||||
return document, nil, nil
|
||||
}
|
||||
}()
|
||||
data := []byte(`{"Type":"com.apple.asset.data","Identifier":"com.example.asset","Payload":{"Reference":{"DataURL":"$FLEET_SECRET_URL"}}}`)
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", data)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked)
|
||||
require.True(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("Expanded payload with authentication key is rejected", func(t *testing.T) {
|
||||
defer reset()
|
||||
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
||||
return `{"Type":"com.apple.asset.data","Identifier":"com.example.asset","Payload":{"Reference":{"DataURL":"https://example.com/data"},"Authentication":{"Username":"u"}}}`, nil, nil
|
||||
}
|
||||
defer func() {
|
||||
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
||||
return document, nil, nil
|
||||
}
|
||||
}()
|
||||
_, err := svc.CreateAppleDDMAsset(ctx, nil, "asset", validData)
|
||||
require.Error(t, err)
|
||||
require.True(t, ds.ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked)
|
||||
require.False(t, ds.CreateAppleDDMAssetFuncInvoked)
|
||||
})
|
||||
}
|
||||
@@ -967,6 +967,38 @@ allow {
|
||||
# Apple MDM
|
||||
##
|
||||
|
||||
# Global admins, maintainers, and gitops can write DDM assets.
|
||||
allow {
|
||||
object.type == "ddm_asset"
|
||||
subject.global_role == [admin, maintainer, gitops][_]
|
||||
action == write
|
||||
}
|
||||
|
||||
# Global admins, maintainers, technicians, and gitops can read DDM assets.
|
||||
allow {
|
||||
object.type == "ddm_asset"
|
||||
subject.global_role == [admin, maintainer, technician, gitops][_]
|
||||
action == read
|
||||
}
|
||||
|
||||
# Team admins, maintainers and gitops can write DDM assets on their team.
|
||||
allow {
|
||||
not is_null(object.team_id)
|
||||
object.team_id != 0
|
||||
object.type == "ddm_asset"
|
||||
team_role(subject, object.team_id) == [admin, maintainer, gitops][_]
|
||||
action == write
|
||||
}
|
||||
|
||||
# Team admins, maintainers, technicians and gitops can read DDM assets on their teams.
|
||||
allow {
|
||||
not is_null(object.team_id)
|
||||
object.team_id != 0
|
||||
object.type == "ddm_asset"
|
||||
team_role(subject, object.team_id) == [admin, maintainer, technician, gitops][_]
|
||||
action == read
|
||||
}
|
||||
|
||||
# Global admins can read, write, and list MDM apple information.
|
||||
allow {
|
||||
object.type == "mdm_apple"
|
||||
|
||||
@@ -2270,6 +2270,111 @@ func TestAuthorizeMDMConfigProfile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorizeDDMAssets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
globalAsset := &fleet.DDMAssetAuthz{}
|
||||
team1Asset := &fleet.DDMAssetAuthz{
|
||||
TeamID: new(uint(1)),
|
||||
}
|
||||
runTestCases(t, []authTestCase{
|
||||
{user: test.UserNoRoles, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserNoRoles, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserNoRoles, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserNoRoles, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserAdmin, object: globalAsset, action: write, allow: true},
|
||||
{user: test.UserAdmin, object: globalAsset, action: read, allow: true},
|
||||
{user: test.UserAdmin, object: team1Asset, action: write, allow: true},
|
||||
{user: test.UserAdmin, object: team1Asset, action: read, allow: true},
|
||||
|
||||
{user: test.UserMaintainer, object: globalAsset, action: write, allow: true},
|
||||
{user: test.UserMaintainer, object: globalAsset, action: read, allow: true},
|
||||
{user: test.UserMaintainer, object: team1Asset, action: write, allow: true},
|
||||
{user: test.UserMaintainer, object: team1Asset, action: read, allow: true},
|
||||
|
||||
{user: test.UserObserver, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserObserver, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserObserver, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserObserver, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserObserverPlus, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserObserverPlus, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserObserverPlus, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserObserverPlus, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserTechnician, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTechnician, object: globalAsset, action: read, allow: true},
|
||||
{user: test.UserTechnician, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTechnician, object: team1Asset, action: read, allow: true},
|
||||
|
||||
{user: test.UserGitOps, object: globalAsset, action: write, allow: true},
|
||||
{user: test.UserGitOps, object: globalAsset, action: read, allow: true},
|
||||
{user: test.UserGitOps, object: team1Asset, action: write, allow: true},
|
||||
{user: test.UserGitOps, object: team1Asset, action: read, allow: true},
|
||||
|
||||
{user: test.UserTeamAdminTeam1, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamAdminTeam1, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamAdminTeam1, object: team1Asset, action: write, allow: true},
|
||||
{user: test.UserTeamAdminTeam1, object: team1Asset, action: read, allow: true},
|
||||
|
||||
{user: test.UserTeamAdminTeam2, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamAdminTeam2, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamAdminTeam2, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTeamAdminTeam2, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserTeamMaintainerTeam1, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamMaintainerTeam1, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamMaintainerTeam1, object: team1Asset, action: write, allow: true},
|
||||
{user: test.UserTeamMaintainerTeam1, object: team1Asset, action: read, allow: true},
|
||||
|
||||
{user: test.UserTeamMaintainerTeam2, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamMaintainerTeam2, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamMaintainerTeam2, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTeamMaintainerTeam2, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserTeamObserverTeam1, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamObserverTeam1, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamObserverTeam1, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTeamObserverTeam1, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserTeamObserverTeam2, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamObserverTeam2, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamObserverTeam2, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTeamObserverTeam2, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserTeamObserverPlusTeam1, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamObserverPlusTeam1, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamObserverPlusTeam1, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTeamObserverPlusTeam1, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserTeamObserverPlusTeam2, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamObserverPlusTeam2, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamObserverPlusTeam2, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTeamObserverPlusTeam2, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserTeamGitOpsTeam1, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam1, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam1, object: team1Asset, action: write, allow: true},
|
||||
{user: test.UserTeamGitOpsTeam1, object: team1Asset, action: read, allow: true},
|
||||
|
||||
{user: test.UserTeamGitOpsTeam2, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam2, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam2, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam2, object: team1Asset, action: read, allow: false},
|
||||
|
||||
{user: test.UserTeamTechnicianTeam1, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamTechnicianTeam1, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamTechnicianTeam1, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTeamTechnicianTeam1, object: team1Asset, action: read, allow: true},
|
||||
|
||||
{user: test.UserTeamTechnicianTeam2, object: globalAsset, action: write, allow: false},
|
||||
{user: test.UserTeamTechnicianTeam2, object: globalAsset, action: read, allow: false},
|
||||
{user: test.UserTeamTechnicianTeam2, object: team1Asset, action: write, allow: false},
|
||||
{user: test.UserTeamTechnicianTeam2, object: team1Asset, action: read, allow: false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorizeMDMAppleSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -7800,3 +7800,120 @@ func (ds *Datastore) GetABMTokenOrgNamesAssociatedByDefaultTeams(ctx context.Con
|
||||
|
||||
return orgNames, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListAppleDDMAssets(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error) {
|
||||
if teamID == nil {
|
||||
teamID = new(uint(0))
|
||||
}
|
||||
|
||||
assets := []*fleet.DDMAsset{}
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &assets, `SELECT asset_uuid, team_id, identifier, name, token, created_at, uploaded_at
|
||||
FROM mdm_apple_declaration_assets WHERE team_id = ?`, teamID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "listing apple ddm assets")
|
||||
}
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetAppleDDMAsset(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
if assetUUID == "" {
|
||||
return nil, ctxerr.New(ctx, "asset UUID is required")
|
||||
}
|
||||
|
||||
var asset fleet.DDMAsset
|
||||
err := sqlx.GetContext(ctx, ds.reader(ctx), &asset, `SELECT asset_uuid, team_id, identifier, name, token, created_at, uploaded_at
|
||||
FROM mdm_apple_declaration_assets WHERE asset_uuid = ?`, assetUUID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, notFound("Asset").WithName(assetUUID)
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting apple ddm asset")
|
||||
}
|
||||
return &asset, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetAppleDDMAssetForDownload(ctx context.Context, assetUUID string) (*fleet.DownloadableDDMAsset, error) {
|
||||
if assetUUID == "" {
|
||||
return nil, ctxerr.New(ctx, "asset UUID is required")
|
||||
}
|
||||
|
||||
var asset fleet.DownloadableDDMAsset
|
||||
err := sqlx.GetContext(ctx, ds.reader(ctx), &asset, `SELECT asset_uuid, team_id, identifier, name, token, created_at, uploaded_at, raw_json
|
||||
FROM mdm_apple_declaration_assets WHERE asset_uuid = ?`, assetUUID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, notFound("Asset").WithName(assetUUID)
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting apple ddm asset")
|
||||
}
|
||||
return &asset, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) CreateAppleDDMAsset(ctx context.Context, name, identifier string, data []byte, teamID *uint) (string, error) {
|
||||
if name == "" {
|
||||
return "", ctxerr.New(ctx, "asset name is required")
|
||||
}
|
||||
if identifier == "" {
|
||||
return "", ctxerr.New(ctx, "asset identifier is required")
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return "", ctxerr.New(ctx, "asset data is required")
|
||||
}
|
||||
|
||||
assetUUID := uuid.NewString()
|
||||
if teamID == nil {
|
||||
teamID = new(uint(0))
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
_, secretsUpdatedAt, err := ds.ExpandEmbeddedSecretsAndUpdatedAt(ctx, string(data))
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "expanding embedded secrets")
|
||||
}
|
||||
|
||||
_, err = ds.writer(ctx).ExecContext(ctx, `
|
||||
INSERT INTO mdm_apple_declaration_assets
|
||||
(asset_uuid, team_id, identifier, name, raw_json, uploaded_at, secrets_updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
assetUUID, teamID, identifier, name, data, now, secretsUpdatedAt)
|
||||
if err != nil {
|
||||
if IsDuplicate(err) {
|
||||
switch {
|
||||
case strings.Contains(err.Error(), "asset_team_name"):
|
||||
return "", alreadyExists("asset_name", name).WithTeamID(*teamID)
|
||||
case strings.Contains(err.Error(), "asset_team_identifier"):
|
||||
return "", alreadyExists("asset_identifier", identifier).WithTeamID(*teamID)
|
||||
}
|
||||
}
|
||||
return "", ctxerr.Wrap(ctx, err, "inserting apple ddm asset")
|
||||
}
|
||||
|
||||
return assetUUID, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) DeleteAppleDDMAsset(ctx context.Context, assetUUID string) error {
|
||||
if assetUUID == "" {
|
||||
return ctxerr.New(ctx, "asset UUID is required")
|
||||
}
|
||||
|
||||
res, err := ds.writer(ctx).ExecContext(ctx, `
|
||||
DELETE FROM mdm_apple_declaration_assets
|
||||
WHERE asset_uuid = ?`, assetUUID)
|
||||
if err != nil {
|
||||
if isMySQLForeignKey(err) {
|
||||
return foreignKey("asset", assetUUID)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "deleting apple ddm asset")
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "checking rows affected for apple ddm asset deletion")
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return ctxerr.Wrap(ctx, notFound("Asset").WithName(assetUUID))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -20,6 +21,11 @@ func TestMDMDDMApple(t *testing.T) {
|
||||
name string
|
||||
fn func(t *testing.T, ds *Datastore)
|
||||
}{
|
||||
{"ListAppleDDMAssets", testListAppleDDMAssets},
|
||||
{"GetAppleDDMAsset", testGetAppleDDMAsset},
|
||||
{"GetAppleDDMAssetForDownload", testGetAppleDDMAssetForDownload},
|
||||
{"CreateAppleDDMAsset", testCreateAppleDDMAsset},
|
||||
{"DeleteAppleDDMAsset", testDeleteAppleDDMAsset},
|
||||
{"StoreDDMStatusReportSkipsRemoveRows", testStoreDDMStatusReportSkipsRemoveRows},
|
||||
{"CleanUpDuplicateRemoveInstallAcrossBatches", testCleanUpDuplicateRemoveInstallAcrossBatches},
|
||||
{"ChannelScopeIsolation", testDDMChannelScopeIsolation},
|
||||
@@ -409,3 +415,204 @@ func testCleanUpDuplicateRemoveInstallAcrossBatches(t *testing.T, ds *Datastore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testListAppleDDMAssets(t *testing.T, ds *Datastore) {
|
||||
t.Run("no assets returns empty list", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
assets, err := ds.ListAppleDDMAssets(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, assets)
|
||||
})
|
||||
|
||||
t.Run("returns assets for requested team", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// insert helper
|
||||
_, err := ds.CreateAppleDDMAsset(ctx, "asset-1", "asset.identifier", []byte(`{"foo":"bar"}`), new(uint(1)))
|
||||
require.NoError(t, err)
|
||||
|
||||
assets, err := ds.ListAppleDDMAssets(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, assets)
|
||||
|
||||
assets, err = ds.ListAppleDDMAssets(ctx, new(uint(1)))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, assets, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetAppleDDMAsset(t *testing.T, ds *Datastore) {
|
||||
t.Run("returns not found for missing asset", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
asset, err := ds.GetAppleDDMAsset(ctx, "fake-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.Nil(t, asset)
|
||||
})
|
||||
|
||||
t.Run("returns asset for existing asset", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
assetUUID, err := ds.CreateAppleDDMAsset(ctx, "asset-1", "asset.identifier", []byte(`{"foo":"bar"}`), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
asset, err := ds.GetAppleDDMAsset(ctx, assetUUID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, asset)
|
||||
})
|
||||
|
||||
t.Run("return error for empty asset uuid", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
asset, err := ds.GetAppleDDMAsset(ctx, "")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "asset UUID is required")
|
||||
require.Nil(t, asset)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetAppleDDMAssetForDownload(t *testing.T, ds *Datastore) {
|
||||
t.Run("returns not found for missing asset", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
asset, err := ds.GetAppleDDMAssetForDownload(ctx, "fake-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.Nil(t, asset)
|
||||
})
|
||||
|
||||
t.Run("returns asset values for existing asset", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
assetName := "asset-1"
|
||||
assetUUID, err := ds.CreateAppleDDMAsset(ctx, assetName, "asset.identifier", []byte(`{"foo":"bar"}`), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
asset, err := ds.GetAppleDDMAssetForDownload(ctx, assetUUID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, asset)
|
||||
require.Equal(t, assetName, asset.Name)
|
||||
require.NotNil(t, asset.Data)
|
||||
})
|
||||
|
||||
t.Run("return error for empty asset uuid", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
asset, err := ds.GetAppleDDMAssetForDownload(ctx, "")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "asset UUID is required")
|
||||
require.Nil(t, asset)
|
||||
})
|
||||
}
|
||||
|
||||
func testDeleteAppleDDMAsset(t *testing.T, ds *Datastore) {
|
||||
t.Run("returns not found for missing asset", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
err := ds.DeleteAppleDDMAsset(ctx, "fake-uuid")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
})
|
||||
|
||||
t.Run("returns error for empty asset UUID", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
err := ds.DeleteAppleDDMAsset(ctx, "")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "asset UUID is required")
|
||||
})
|
||||
|
||||
t.Run("deletes existing asset", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
assetUUID, err := ds.CreateAppleDDMAsset(ctx, "asset-1", "asset.identifier", []byte(`{"foo":"bar"}`), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.DeleteAppleDDMAsset(ctx, assetUUID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("returns foreign key error for asset with declaration association", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
assetUUID, err := ds.CreateAppleDDMAsset(ctx, "asset-1", "asset.identifier", []byte(`{"foo":"bar"}`), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert a declaration, and decl<->asset association.
|
||||
declUUID := uuid.NewString()
|
||||
decl, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
|
||||
DeclarationUUID: declUUID,
|
||||
Identifier: "declaration.identifier",
|
||||
Name: "decl-name",
|
||||
RawJSON: []byte(`{"foo":"bar"}`),
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err = q.ExecContext(ctx, `INSERT INTO mdm_apple_declaration_asset_references (declaration_uuid, asset_uuid) VALUES (?, ?)`, decl.DeclarationUUID, assetUUID)
|
||||
require.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
|
||||
err = ds.DeleteAppleDDMAsset(ctx, assetUUID)
|
||||
require.Error(t, err)
|
||||
var foreignKeyErr *foreignKeyError
|
||||
require.ErrorAs(t, err, &foreignKeyErr)
|
||||
})
|
||||
}
|
||||
|
||||
func testCreateAppleDDMAsset(t *testing.T, ds *Datastore) {
|
||||
t.Run("creates asset with valid data", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
assetUUID, err := ds.CreateAppleDDMAsset(ctx, "valid-asset", "valid-asset-identifier", []byte(`{"foo":"bar"}`), nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, assetUUID)
|
||||
})
|
||||
|
||||
t.Run("fails to create asset with empty name", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
_, err := ds.CreateAppleDDMAsset(ctx, "", "asset.identifier", []byte(`{"foo":"bar"}`), nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "asset name is required")
|
||||
})
|
||||
|
||||
t.Run("fails to create asset with empty identifier", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
_, err := ds.CreateAppleDDMAsset(ctx, "asset-1", "", []byte(`{"foo":"bar"}`), nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "asset identifier is required")
|
||||
})
|
||||
|
||||
t.Run("fails to create asset with empty data", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
_, err := ds.CreateAppleDDMAsset(ctx, "asset-1", "asset.identifier", nil, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "asset data is required")
|
||||
})
|
||||
|
||||
t.Run("returns already exists error when creating asset with duplicate identifier", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
assetIdentifier := "conflict.asset.identifier"
|
||||
_, err := ds.CreateAppleDDMAsset(ctx, "conflict-asset-1", assetIdentifier, []byte(`{"foo":"bar"}`), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ds.CreateAppleDDMAsset(ctx, "asset-2", assetIdentifier, []byte(`{"foo":"baz"}`), nil)
|
||||
require.Error(t, err)
|
||||
var alreadyExistsErr *existsError
|
||||
require.ErrorAs(t, err, &alreadyExistsErr)
|
||||
})
|
||||
|
||||
t.Run("returns already exists error when creating asset with duplicate name", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
assetName := "conflict-asset-name"
|
||||
_, err := ds.CreateAppleDDMAsset(ctx, assetName, "conflict.asset.identifier-one", []byte(`{"foo":"bar"}`), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ds.CreateAppleDDMAsset(ctx, assetName, "conflict.asset.identifier-2", []byte(`{"foo":"baz"}`), nil)
|
||||
require.Error(t, err)
|
||||
var alreadyExistsErr *existsError
|
||||
require.ErrorAs(t, err, &alreadyExistsErr)
|
||||
})
|
||||
|
||||
t.Run("does not conflict across teams", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
assetIdentifier := "no-conflict.asset.identifier"
|
||||
assetName := "no-conflict.asset-1"
|
||||
_, err := ds.CreateAppleDDMAsset(ctx, assetName, assetIdentifier, []byte(`{"foo":"bar"}`), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ds.CreateAppleDDMAsset(ctx, assetName, assetIdentifier, []byte(`{"foo":"baz"}`), new(uint(1)))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1525,3 +1525,51 @@ type ADUEEnrollmentChallenge struct {
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
UsedAt *time.Time `db:"used_at"`
|
||||
}
|
||||
|
||||
// DDMAsset is the JSON representation of an asset, only excluding the raw json.
|
||||
type DDMAsset struct {
|
||||
AssetUUID string `db:"asset_uuid" json:"asset_uuid"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Identifier string `db:"identifier" json:"identifier"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UploadedAt *time.Time `db:"uploaded_at" json:"uploaded_at"`
|
||||
Checksum []byte `db:"token" json:"checksum"`
|
||||
TeamID *uint `db:"team_id" json:"-"` // Retrieve team ID for logic, but do not return it in JSON.
|
||||
}
|
||||
|
||||
// DownloadableDDMAsset is a service struct that contains the DDMAsset for logic checks, and the raw JSON for serving back to the caller.
|
||||
type DownloadableDDMAsset struct {
|
||||
DDMAsset
|
||||
Data []byte `db:"raw_json" json:"-"`
|
||||
}
|
||||
|
||||
type RawDDMAsset struct {
|
||||
Type string `json:"Type"`
|
||||
Identifier string `json:"Identifier"`
|
||||
Payload RawDDMAssetPayload `json:"Payload"`
|
||||
}
|
||||
|
||||
type RawDDMAssetPayload struct {
|
||||
Reference RawDDMAssetPayloadReference `json:"Reference"`
|
||||
Authentication json.RawMessage `json:"Authentication"` // We don't care about the inner, we use it for checking existence
|
||||
}
|
||||
|
||||
// Struct describing the AssetData reference payload structure
|
||||
// https://developer.apple.com/documentation/devicemanagement/assetdatareferenceobject (asset data is used as an example here)
|
||||
type RawDDMAssetPayloadReference struct {
|
||||
ContentType string `json:"ContentType,omitempty"`
|
||||
DataURL string `json:"DataURL"`
|
||||
HashSHA256 string `json:"Hash-SHA-256,omitempty"`
|
||||
Size int64 `json:"Size,omitempty"`
|
||||
}
|
||||
|
||||
// DDMAssetAuthz is used to check user authorization to read/write an
|
||||
// DDM asset.
|
||||
type DDMAssetAuthz struct {
|
||||
TeamID *uint `json:"team_id" renameto:"fleet_id"` // required for authorization by team
|
||||
}
|
||||
|
||||
// AuthzType implements authz.AuthzTyper.
|
||||
func (d DDMAssetAuthz) AuthzType() string {
|
||||
return "ddm_asset"
|
||||
}
|
||||
|
||||
@@ -3555,6 +3555,12 @@ type Datastore interface {
|
||||
ConsumeADUEEnrollmentChallenge(ctx context.Context, challenge string) (*ADUEEnrollmentChallenge, error)
|
||||
// CleanupExpiredADUEEnrollmentChallenges deletes enrollment challenges expired more than 1 day ago.
|
||||
CleanupExpiredADUEEnrollmentChallenges(ctx context.Context) error
|
||||
|
||||
ListAppleDDMAssets(ctx context.Context, teamID *uint) ([]*DDMAsset, error)
|
||||
GetAppleDDMAsset(ctx context.Context, assetUUID string) (*DDMAsset, error)
|
||||
GetAppleDDMAssetForDownload(ctx context.Context, assetUUID string) (*DownloadableDDMAsset, error)
|
||||
CreateAppleDDMAsset(ctx context.Context, name, identifier string, data []byte, teamID *uint) (string, error)
|
||||
DeleteAppleDDMAsset(ctx context.Context, assetUUID string) error
|
||||
}
|
||||
|
||||
type AndroidDatastore interface {
|
||||
|
||||
@@ -12,6 +12,7 @@ const (
|
||||
MaxBatchScriptSize int64 = 25 * units.MiB
|
||||
MaxProfileSize int64 = 1.5 * units.MiB // 1.5 to allow for roughly 1MB content, and B64 encoding
|
||||
MaxBatchProfileSize int64 = 25 * units.MiB
|
||||
MaxMDMAssetSize int64 = 1.5 * units.MiB // 1.5 to allow for roughly 1MB content, and B64 encoding
|
||||
MaxEULASize int64 = 25 * units.MiB
|
||||
MaxSoftwareBatchSize int64 = 25 * units.MiB // Takes multiple installers, with scripts and queries
|
||||
MaxMDMCommandSize int64 = 2 * units.MiB
|
||||
|
||||
@@ -1544,6 +1544,20 @@ type Service interface {
|
||||
|
||||
// UnenrollMDM unenrolls the host from MDM
|
||||
UnenrollMDM(ctx context.Context, hostID uint) error
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Apple MDM Assets
|
||||
|
||||
// ListAppleDDMAssets returns a list of assets used for Apple DDM belonging to the specified team, in their API representation.
|
||||
ListAppleDDMAssets(ctx context.Context, teamID *uint) ([]*DDMAsset, error)
|
||||
// GetAppleDDMAsset returns the asset with the given UUID, in its API representation.
|
||||
GetAppleDDMAsset(ctx context.Context, assetUUID string) (*DDMAsset, error)
|
||||
// DownloadAppleDDMAsset returns the filename and contents of the asset with the given UUID.
|
||||
DownloadAppleDDMAsset(ctx context.Context, assetUUID string) (filename string, data []byte, err error)
|
||||
// CreateAppleDDMAsset creates a new asset used for Apple DDM. It returns the UUID of the created asset.
|
||||
CreateAppleDDMAsset(ctx context.Context, teamID *uint, name string, data []byte) (string, error)
|
||||
// DeleteAppleDDMAsset deletes the asset with the given UUID.
|
||||
DeleteAppleDDMAsset(ctx context.Context, assetUUID string) error
|
||||
}
|
||||
|
||||
type KeyValueStore interface {
|
||||
|
||||
@@ -2148,6 +2148,16 @@ type ConsumeADUEEnrollmentChallengeFunc func(ctx context.Context, challenge stri
|
||||
|
||||
type CleanupExpiredADUEEnrollmentChallengesFunc func(ctx context.Context) error
|
||||
|
||||
type ListAppleDDMAssetsFunc func(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error)
|
||||
|
||||
type GetAppleDDMAssetFunc func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error)
|
||||
|
||||
type GetAppleDDMAssetForDownloadFunc func(ctx context.Context, assetUUID string) (*fleet.DownloadableDDMAsset, error)
|
||||
|
||||
type CreateAppleDDMAssetFunc func(ctx context.Context, name string, identifier string, data []byte, teamID *uint) (string, error)
|
||||
|
||||
type DeleteAppleDDMAssetFunc func(ctx context.Context, assetUUID string) error
|
||||
|
||||
type DataStore struct {
|
||||
AppConfigFunc AppConfigFunc
|
||||
AppConfigFuncInvoked bool
|
||||
@@ -5335,6 +5345,21 @@ type DataStore struct {
|
||||
CleanupExpiredADUEEnrollmentChallengesFunc CleanupExpiredADUEEnrollmentChallengesFunc
|
||||
CleanupExpiredADUEEnrollmentChallengesFuncInvoked bool
|
||||
|
||||
ListAppleDDMAssetsFunc ListAppleDDMAssetsFunc
|
||||
ListAppleDDMAssetsFuncInvoked bool
|
||||
|
||||
GetAppleDDMAssetFunc GetAppleDDMAssetFunc
|
||||
GetAppleDDMAssetFuncInvoked bool
|
||||
|
||||
GetAppleDDMAssetForDownloadFunc GetAppleDDMAssetForDownloadFunc
|
||||
GetAppleDDMAssetForDownloadFuncInvoked bool
|
||||
|
||||
CreateAppleDDMAssetFunc CreateAppleDDMAssetFunc
|
||||
CreateAppleDDMAssetFuncInvoked bool
|
||||
|
||||
DeleteAppleDDMAssetFunc DeleteAppleDDMAssetFunc
|
||||
DeleteAppleDDMAssetFuncInvoked bool
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -12771,3 +12796,38 @@ func (s *DataStore) CleanupExpiredADUEEnrollmentChallenges(ctx context.Context)
|
||||
s.mu.Unlock()
|
||||
return s.CleanupExpiredADUEEnrollmentChallengesFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListAppleDDMAssets(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error) {
|
||||
s.mu.Lock()
|
||||
s.ListAppleDDMAssetsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ListAppleDDMAssetsFunc(ctx, teamID)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetAppleDDMAsset(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
s.mu.Lock()
|
||||
s.GetAppleDDMAssetFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetAppleDDMAssetFunc(ctx, assetUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetAppleDDMAssetForDownload(ctx context.Context, assetUUID string) (*fleet.DownloadableDDMAsset, error) {
|
||||
s.mu.Lock()
|
||||
s.GetAppleDDMAssetForDownloadFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetAppleDDMAssetForDownloadFunc(ctx, assetUUID)
|
||||
}
|
||||
|
||||
func (s *DataStore) CreateAppleDDMAsset(ctx context.Context, name string, identifier string, data []byte, teamID *uint) (string, error) {
|
||||
s.mu.Lock()
|
||||
s.CreateAppleDDMAssetFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.CreateAppleDDMAssetFunc(ctx, name, identifier, data, teamID)
|
||||
}
|
||||
|
||||
func (s *DataStore) DeleteAppleDDMAsset(ctx context.Context, assetUUID string) error {
|
||||
s.mu.Lock()
|
||||
s.DeleteAppleDDMAssetFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.DeleteAppleDDMAssetFunc(ctx, assetUUID)
|
||||
}
|
||||
|
||||
@@ -942,6 +942,16 @@ type GetGroupedCertificateAuthoritiesFunc func(ctx context.Context, includeSecre
|
||||
|
||||
type UnenrollMDMFunc func(ctx context.Context, hostID uint) error
|
||||
|
||||
type ListAppleDDMAssetsFunc func(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error)
|
||||
|
||||
type GetAppleDDMAssetFunc func(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error)
|
||||
|
||||
type DownloadAppleDDMAssetFunc func(ctx context.Context, assetUUID string) (filename string, data []byte, err error)
|
||||
|
||||
type CreateAppleDDMAssetFunc func(ctx context.Context, teamID *uint, name string, data []byte) (string, error)
|
||||
|
||||
type DeleteAppleDDMAssetFunc func(ctx context.Context, assetUUID string) error
|
||||
|
||||
type Service struct {
|
||||
EnrollOsqueryFunc EnrollOsqueryFunc
|
||||
EnrollOsqueryFuncInvoked bool
|
||||
@@ -2326,6 +2336,21 @@ type Service struct {
|
||||
UnenrollMDMFunc UnenrollMDMFunc
|
||||
UnenrollMDMFuncInvoked bool
|
||||
|
||||
ListAppleDDMAssetsFunc ListAppleDDMAssetsFunc
|
||||
ListAppleDDMAssetsFuncInvoked bool
|
||||
|
||||
GetAppleDDMAssetFunc GetAppleDDMAssetFunc
|
||||
GetAppleDDMAssetFuncInvoked bool
|
||||
|
||||
DownloadAppleDDMAssetFunc DownloadAppleDDMAssetFunc
|
||||
DownloadAppleDDMAssetFuncInvoked bool
|
||||
|
||||
CreateAppleDDMAssetFunc CreateAppleDDMAssetFunc
|
||||
CreateAppleDDMAssetFuncInvoked bool
|
||||
|
||||
DeleteAppleDDMAssetFunc DeleteAppleDDMAssetFunc
|
||||
DeleteAppleDDMAssetFuncInvoked bool
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -5555,3 +5580,38 @@ func (s *Service) UnenrollMDM(ctx context.Context, hostID uint) error {
|
||||
s.mu.Unlock()
|
||||
return s.UnenrollMDMFunc(ctx, hostID)
|
||||
}
|
||||
|
||||
func (s *Service) ListAppleDDMAssets(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error) {
|
||||
s.mu.Lock()
|
||||
s.ListAppleDDMAssetsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ListAppleDDMAssetsFunc(ctx, teamID)
|
||||
}
|
||||
|
||||
func (s *Service) GetAppleDDMAsset(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
s.mu.Lock()
|
||||
s.GetAppleDDMAssetFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetAppleDDMAssetFunc(ctx, assetUUID)
|
||||
}
|
||||
|
||||
func (s *Service) DownloadAppleDDMAsset(ctx context.Context, assetUUID string) (filename string, data []byte, err error) {
|
||||
s.mu.Lock()
|
||||
s.DownloadAppleDDMAssetFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.DownloadAppleDDMAssetFunc(ctx, assetUUID)
|
||||
}
|
||||
|
||||
func (s *Service) CreateAppleDDMAsset(ctx context.Context, teamID *uint, name string, data []byte) (string, error) {
|
||||
s.mu.Lock()
|
||||
s.CreateAppleDDMAssetFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.CreateAppleDDMAssetFunc(ctx, teamID, name, data)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteAppleDDMAsset(ctx context.Context, assetUUID string) error {
|
||||
s.mu.Lock()
|
||||
s.DeleteAppleDDMAssetFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.DeleteAppleDDMAssetFunc(ctx, assetUUID)
|
||||
}
|
||||
|
||||
@@ -3669,6 +3669,207 @@ func (svc *Service) MDMAppleDisableFileVaultAndEscrow(ctx context.Context, teamI
|
||||
return fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
type listAppleDDMAssetsRequest struct {
|
||||
TeamID *uint `query:"fleet_id,optional"`
|
||||
}
|
||||
|
||||
type listAppleDDMAssetsResponse struct {
|
||||
Assets []*fleet.DDMAsset `json:"assets"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r listAppleDDMAssetsResponse) Error() error { return r.Err }
|
||||
|
||||
func listAppleDDMAssetsEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*listAppleDDMAssetsRequest)
|
||||
assets, err := svc.ListAppleDDMAssets(ctx, req.TeamID)
|
||||
if err != nil {
|
||||
return listAppleDDMAssetsResponse{Err: err}, nil
|
||||
}
|
||||
return listAppleDDMAssetsResponse{Assets: assets}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ListAppleDDMAssets(ctx context.Context, teamID *uint) ([]*fleet.DDMAsset, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
type getAppleDDMAssetRequest struct {
|
||||
AssetUUID string `url:"asset_uuid"`
|
||||
Alt string `query:"alt,optional"`
|
||||
}
|
||||
|
||||
func (r getAppleDDMAssetRequest) ValidateRequest() error {
|
||||
if r.Alt != "" && strings.ToLower(r.Alt) != "media" {
|
||||
return &fleet.BadRequestError{Message: "Alt query param value is invalid. Supported values are empty and \"media\""}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type getAppleDDMAssetResponse struct {
|
||||
Asset *fleet.DDMAsset `json:",omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r getAppleDDMAssetResponse) Error() error { return r.Err }
|
||||
|
||||
type downloadAppleDDMAssetResponse struct {
|
||||
Name string
|
||||
Data []byte
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r downloadAppleDDMAssetResponse) Error() error { return r.Err }
|
||||
|
||||
func (r downloadAppleDDMAssetResponse) HijackRender(ctx context.Context, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(r.Data)))
|
||||
w.Header().Set("Content-Type", "application/json") // We know we return JSON, if we ever serve other files we need a generic octet-stream
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename=%q`, r.Name)) // make the caller download the file
|
||||
if n, err := w.Write(r.Data); err != nil {
|
||||
logging.WithExtras(ctx, "err", err, "written", n)
|
||||
}
|
||||
}
|
||||
|
||||
func getAppleDDMAssetEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*getAppleDDMAssetRequest)
|
||||
|
||||
if strings.ToLower(req.Alt) == "media" {
|
||||
name, data, err := svc.DownloadAppleDDMAsset(ctx, req.AssetUUID)
|
||||
if err != nil {
|
||||
return downloadAppleDDMAssetResponse{Err: err}, nil
|
||||
}
|
||||
return downloadAppleDDMAssetResponse{Name: name, Data: data}, nil
|
||||
}
|
||||
|
||||
asset, err := svc.GetAppleDDMAsset(ctx, req.AssetUUID)
|
||||
if err != nil {
|
||||
return getAppleDDMAssetResponse{Err: err}, nil
|
||||
}
|
||||
return getAppleDDMAssetResponse{Asset: asset}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetAppleDDMAsset(ctx context.Context, assetUUID string) (*fleet.DDMAsset, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
func (svc *Service) DownloadAppleDDMAsset(ctx context.Context, assetUUID string) (name string, data []byte, err error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return "", nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
type createAppleDDMAssetRequest struct {
|
||||
TeamID *uint
|
||||
Asset *multipart.FileHeader
|
||||
}
|
||||
|
||||
func (createAppleDDMAssetRequest) DecodeRequest(ctx context.Context, r *http.Request) (any, error) {
|
||||
decoded := new(createAppleDDMAssetRequest)
|
||||
|
||||
err := parseMultipartForm(ctx, r, platform_http.MaxMultipartFormSize)
|
||||
if err != nil {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "failed to parse multipart form",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
val, ok := r.MultipartForm.Value["fleet_id"]
|
||||
if !ok || len(val) < 1 {
|
||||
// default is no team
|
||||
decoded.TeamID = new(uint(0))
|
||||
} else {
|
||||
fleetID, err := strconv.ParseUint(val[0], 10, 32)
|
||||
if err != nil {
|
||||
return nil, &fleet.BadRequestError{Message: fmt.Sprintf("Invalid fleet_id: %s", val[0])}
|
||||
}
|
||||
decoded.TeamID = new(uint(fleetID))
|
||||
}
|
||||
|
||||
fhs, ok := r.MultipartForm.File["asset"]
|
||||
if !ok || len(fhs) < 1 {
|
||||
return nil, &fleet.BadRequestError{Message: "no file headers for asset"}
|
||||
}
|
||||
decoded.Asset = fhs[0]
|
||||
|
||||
if !strings.HasSuffix(decoded.Asset.Filename, ".json") {
|
||||
return nil, &fleet.BadRequestError{Message: "Invalid file type for asset. Only \".json\" files are allowed"}
|
||||
}
|
||||
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
type createAppleDDMAssetResponse struct {
|
||||
AssetUUID string `json:"asset_uuid,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r createAppleDDMAssetResponse) Error() error { return r.Err }
|
||||
|
||||
func createAppleDDMAssetEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*createAppleDDMAssetRequest)
|
||||
f, err := req.Asset.Open()
|
||||
if err != nil {
|
||||
return createAppleDDMAssetResponse{Err: err}, nil
|
||||
}
|
||||
defer f.Close()
|
||||
assetData, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return createAppleDDMAssetResponse{Err: err}, nil
|
||||
}
|
||||
assetName := strings.TrimSuffix(req.Asset.Filename, ".json")
|
||||
assetUUID, err := svc.CreateAppleDDMAsset(ctx, req.TeamID, assetName, assetData)
|
||||
if err != nil {
|
||||
return createAppleDDMAssetResponse{Err: err}, nil
|
||||
}
|
||||
return createAppleDDMAssetResponse{AssetUUID: assetUUID}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) CreateAppleDDMAsset(ctx context.Context, teamID *uint, name string, data []byte) (string, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return "", fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
type deleteAppleDDMAssetRequest struct {
|
||||
AssetUUID string `url:"asset_uuid"`
|
||||
}
|
||||
|
||||
type deleteAppleDDMAssetResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r deleteAppleDDMAssetResponse) Error() error { return r.Err }
|
||||
|
||||
func (r deleteAppleDDMAssetResponse) Status() int { return http.StatusNoContent }
|
||||
|
||||
func deleteAppleDDMAssetEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*deleteAppleDDMAssetRequest)
|
||||
if err := svc.DeleteAppleDDMAsset(ctx, req.AssetUUID); err != nil {
|
||||
return deleteAppleDDMAssetResponse{Err: err}, nil
|
||||
}
|
||||
return deleteAppleDDMAssetResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) DeleteAppleDDMAsset(ctx context.Context, assetUUID string) error {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Implementation of nanomdm's CheckinAndCommandService interface
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -772,6 +772,12 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
mdmAppleMW.WithRequestBodySizeLimit(fleet.MaxProfileSize).POST("/api/_version_/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileEndpoint, preassignMDMAppleProfileRequest{})
|
||||
mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentEndpoint, matchMDMApplePreassignmentRequest{})
|
||||
|
||||
// This section handles MDM "assets", specifically for Apple DDM.
|
||||
mdmAppleMW.GET("/api/_version_/fleet/assets", listAppleDDMAssetsEndpoint, listAppleDDMAssetsRequest{})
|
||||
mdmAppleMW.GET("/api/_version_/fleet/assets/{asset_uuid}", getAppleDDMAssetEndpoint, getAppleDDMAssetRequest{})
|
||||
mdmAppleMW.WithRequestBodySizeLimit(fleet.MaxMDMAssetSize).POST("/api/_version_/fleet/assets", createAppleDDMAssetEndpoint, createAppleDDMAssetRequest{})
|
||||
mdmAppleMW.DELETE("/api/_version_/fleet/assets/{asset_uuid}", deleteAppleDDMAssetEndpoint, deleteAppleDDMAssetRequest{})
|
||||
|
||||
mdmAnyMW := ue.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAnyMDM())
|
||||
|
||||
mdmAnyMW.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/configuration_profiles", getHostProfilesEndpoint, getHostProfilesRequest{})
|
||||
|
||||
Reference in New Issue
Block a user