Add ability to upload EULA via gitops (#30332)
relates to [#28691](https://github.com/fleetdm/fleet/issues/28691) This adds the ability to upload the EULA users see during the setup experience via gitops. It follows patterns used for uploading the bootstrap package via gitops. I've also added a sha256 column to the `eulas` table in order to easily compare the existing eula with a new one to see if we need to perform an upload. Finally I added the support to generate this new gitops setting with the `generate-gitops` command - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] If database migrations are included, checked table schema to confirm autoupdate - For new Fleet configuration settings - [x] Verified that the setting can be managed via GitOps, or confirmed that the setting is explicitly being excluded from GitOps. If managing via Gitops: - [x] Verified that the setting is exported via `fleetctl generate-gitops` - [x] Added the setting to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - [x] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [x] Verified that any relevant UI is disabled when GitOps mode is enabled - For database migrations: - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- add ability to add EULA end user sees during setup experience via gitops
|
||||
@@ -63,6 +63,8 @@ type generateGitopsClient interface {
|
||||
ListConfigurationProfiles(teamID *uint) ([]*fleet.MDMConfigProfilePayload, error)
|
||||
GetScriptContents(scriptID uint) ([]byte, error)
|
||||
GetProfileContents(profileID string) ([]byte, error)
|
||||
GetEULAMetadata() (*fleet.MDMEULA, error)
|
||||
GetEULAContent(token string) ([]byte, error)
|
||||
GetTeam(teamID uint) (*fleet.Team, error)
|
||||
ListSoftwareTitles(query string) ([]fleet.SoftwareTitleListResult, error)
|
||||
GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTitle, error)
|
||||
@@ -750,8 +752,43 @@ func (cmd *GenerateGitopsCommand) generateIntegrations(filePath string, integrat
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (cmd *GenerateGitopsCommand) generateEULA() (string, error) {
|
||||
// Download the eula metadata for the token.
|
||||
eulaMetadata, err := cmd.Client.GetEULAMetadata()
|
||||
if err != nil {
|
||||
// not found is OK, it means the user has not uploaded a EULA yet.
|
||||
if strings.Contains(err.Error(), "Resource Not Found") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(cmd.CLI.App.ErrWriter, "Error getting eula metadata: %s\n", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// now we want the eula contents, which is a PDF.
|
||||
eulaContent, err := cmd.Client.GetEULAContent(eulaMetadata.Token)
|
||||
if err != nil {
|
||||
fmt.Fprintf(cmd.CLI.App.ErrWriter, "Error getting eula contents: %s\n", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("lib/eula/%s", eulaMetadata.Name)
|
||||
cmd.FilesToWrite[fileName] = string(eulaContent)
|
||||
path := fmt.Sprintf("./%s", fileName)
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// This struct is used to represent the MDM configuration that is used with GitOps.
|
||||
// It includes an additonal end user license agreement (EULA) field, which is
|
||||
// not present in the fleet.MDM struct.
|
||||
type gitopsMDM struct {
|
||||
fleet.MDM
|
||||
EndUserLicenseAgreement string `json:"end_user_license_agreement,omitempty"`
|
||||
}
|
||||
|
||||
func (cmd *GenerateGitopsCommand) generateMDM(mdm *fleet.MDM) (map[string]interface{}, error) {
|
||||
t := reflect.TypeOf(fleet.MDM{})
|
||||
t := reflect.TypeOf(gitopsMDM{})
|
||||
result := map[string]interface{}{
|
||||
jsonFieldName(t, "AppleServerURL"): mdm.AppleServerURL,
|
||||
jsonFieldName(t, "EndUserAuthentication"): mdm.EndUserAuthentication,
|
||||
@@ -759,6 +796,13 @@ func (cmd *GenerateGitopsCommand) generateMDM(mdm *fleet.MDM) (map[string]interf
|
||||
if cmd.AppConfig.License.IsPremium() {
|
||||
result[jsonFieldName(t, "AppleBusinessManager")] = mdm.AppleBusinessManager
|
||||
result[jsonFieldName(t, "VolumePurchasingProgram")] = mdm.VolumePurchasingProgram
|
||||
|
||||
eulaPath, err := cmd.generateEULA()
|
||||
if err != nil {
|
||||
fmt.Fprintf(cmd.CLI.App.ErrWriter, "Error generating EULA: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
result[jsonFieldName(t, "EndUserLicenseAgreement")] = eulaPath
|
||||
}
|
||||
|
||||
if !cmd.CLI.Bool("insecure") {
|
||||
|
||||
@@ -378,6 +378,17 @@ func (MockClient) Me() (*fleet.User, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (MockClient) GetEULAMetadata() (*fleet.MDMEULA, error) {
|
||||
return &fleet.MDMEULA{
|
||||
Name: "test.pdf",
|
||||
Token: "test-eula-token",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (MockClient) GetEULAContent(token string) ([]byte, error) {
|
||||
return []byte("This is the EULA content."), nil
|
||||
}
|
||||
|
||||
func compareDirs(t *testing.T, sourceDir, targetDir string) {
|
||||
err := filepath.WalkDir(sourceDir, func(srcPath string, d os.DirEntry, walkErr error) error {
|
||||
if d.IsDir() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package fleetctl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
|
||||
mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
digicert_mock "github.com/fleetdm/fleet/v4/server/mock/digicert"
|
||||
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
|
||||
scep_mock "github.com/fleetdm/fleet/v4/server/mock/scep"
|
||||
@@ -3084,3 +3086,209 @@ func TestGitOpsNoTeamConditionalAccess(t *testing.T) {
|
||||
require.True(t, appConfig.Integrations.ConditionalAccessEnabled.Set)
|
||||
require.False(t, appConfig.Integrations.ConditionalAccessEnabled.Value)
|
||||
}
|
||||
|
||||
func TestGitOpsEULASetting(t *testing.T) {
|
||||
createGlobalGitOpsConfig := func(mdm string) string {
|
||||
return fmt.Sprintf(`
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
software:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: "https://foo.example.com"
|
||||
org_info:
|
||||
org_name: GitOps Test
|
||||
secrets:
|
||||
- secret: "global"
|
||||
mdm:
|
||||
%s
|
||||
`, mdm)
|
||||
}
|
||||
|
||||
// Create a temporary PDF file
|
||||
pdfContent := []byte("%PDF-1\npdf-test")
|
||||
tmpPDF, err := os.CreateTemp(t.TempDir(), "*.pdf")
|
||||
require.NoError(t, err)
|
||||
// Write a minimal valid PDF header so the file is recognized as a PDF.
|
||||
_, err = tmpPDF.Write(pdfContent)
|
||||
require.NoError(t, err)
|
||||
pdfPath, err := filepath.Abs(tmpPDF.Name())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an invalid temp PDF file
|
||||
tmpInvalidPDF, err := os.CreateTemp(t.TempDir(), "*.txt")
|
||||
require.NoError(t, err)
|
||||
_, err = tmpPDF.Write([]byte("not-a-pdf"))
|
||||
require.NoError(t, err)
|
||||
invalidPDFPath, err := filepath.Abs(tmpInvalidPDF.Name())
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg string
|
||||
mockSetup func(t *testing.T, ds *mock.Store)
|
||||
dryRunAssertion func(t *testing.T, ds *mock.Store, out string, err error)
|
||||
realRunAssertion func(t *testing.T, ds *mock.Store, out string, err error)
|
||||
}{
|
||||
{
|
||||
name: "valid pdf file (no existing EULA uploaded)",
|
||||
cfg: createGlobalGitOpsConfig(fmt.Sprintf(`end_user_license_agreement: "%s"`, pdfPath)),
|
||||
mockSetup: func(t *testing.T, ds *mock.Store) {
|
||||
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
||||
return nil, ¬FoundError{} // No existing EULA
|
||||
}
|
||||
},
|
||||
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] would've applied EULA")
|
||||
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
||||
},
|
||||
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] applied EULA")
|
||||
assert.True(t, ds.MDMInsertEULAFuncInvoked)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid new pdf file (different EULA already uploaded)",
|
||||
cfg: createGlobalGitOpsConfig(fmt.Sprintf(`end_user_license_agreement: "%s"`, pdfPath)),
|
||||
mockSetup: func(t *testing.T, ds *mock.Store) {
|
||||
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
||||
return &fleet.MDMEULA{
|
||||
Name: pdfPath,
|
||||
Token: "test-token",
|
||||
}, nil
|
||||
}
|
||||
},
|
||||
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] would've applied EULA")
|
||||
assert.False(t, ds.MDMDeleteEULAFuncInvoked)
|
||||
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
||||
},
|
||||
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] applied EULA")
|
||||
assert.True(t, ds.MDMDeleteEULAFuncInvoked) // deleted old EULA
|
||||
assert.True(t, ds.MDMInsertEULAFuncInvoked) // new EULA was updated
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no EULA specified (no existing EULA uploaded)",
|
||||
cfg: createGlobalGitOpsConfig(""),
|
||||
mockSetup: func(t *testing.T, ds *mock.Store) {
|
||||
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
||||
return nil, ¬FoundError{} // No existing EULA
|
||||
}
|
||||
},
|
||||
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] would've applied EULA")
|
||||
assert.False(t, ds.MDMDeleteEULAFuncInvoked)
|
||||
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
||||
},
|
||||
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] applied EULA")
|
||||
assert.False(t, ds.MDMDeleteEULAFuncInvoked) // no EULA to delete
|
||||
assert.False(t, ds.MDMInsertEULAFuncInvoked) // no EULA to upload
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deleting existing EULA",
|
||||
cfg: createGlobalGitOpsConfig(""),
|
||||
mockSetup: func(t *testing.T, ds *mock.Store) {
|
||||
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
||||
return &fleet.MDMEULA{
|
||||
Name: pdfPath,
|
||||
Token: "test-token",
|
||||
}, nil
|
||||
}
|
||||
},
|
||||
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] would've applied EULA")
|
||||
assert.False(t, ds.MDMDeleteEULAFuncInvoked)
|
||||
},
|
||||
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] applied EULA")
|
||||
assert.True(t, ds.MDMDeleteEULAFuncInvoked) // deleted EULA
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not a PDF file",
|
||||
cfg: createGlobalGitOpsConfig(fmt.Sprintf(`end_user_license_agreement: "%s"`, invalidPDFPath)),
|
||||
mockSetup: func(t *testing.T, ds *mock.Store) {
|
||||
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
||||
return nil, ¬FoundError{} // No existing EULA
|
||||
}
|
||||
},
|
||||
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.ErrorContains(t, err, "invalid file type")
|
||||
},
|
||||
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.ErrorContains(t, err, "invalid file type")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uploading the same EULA again",
|
||||
cfg: createGlobalGitOpsConfig(""),
|
||||
mockSetup: func(t *testing.T, ds *mock.Store) {
|
||||
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
||||
hash := sha256.Sum256(pdfContent) // Simulate same EULA
|
||||
return &fleet.MDMEULA{
|
||||
Name: pdfPath,
|
||||
Token: "test-token",
|
||||
Sha256: hash[:], // Simulate same EULA
|
||||
}, nil
|
||||
}
|
||||
},
|
||||
dryRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] would've applied EULA")
|
||||
assert.False(t, ds.MDMInsertEULAFuncInvoked)
|
||||
},
|
||||
realRunAssertion: func(t *testing.T, ds *mock.Store, out string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "[+] applied EULA")
|
||||
assert.False(t, ds.MDMInsertEULAFuncInvoked) // No new EULA uploaded
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
|
||||
|
||||
// these mocks are used for all tests
|
||||
ds.MDMInsertEULAFunc = func(ctx context.Context, eula *fleet.MDMEULA) error {
|
||||
return nil
|
||||
}
|
||||
ds.MDMDeleteEULAFunc = func(ctx context.Context, token string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// these mocks are defined in the individual test cases
|
||||
tt.mockSetup(t, ds)
|
||||
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||||
require.NoError(t, err)
|
||||
_, err = tmpFile.WriteString(tt.cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Dry run
|
||||
out, err := RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
|
||||
tt.dryRunAssertion(t, ds, out.String(), err)
|
||||
if t.Failed() {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// Real run
|
||||
out, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
|
||||
tt.realRunAssertion(t, ds, out.String(), err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,12 +1,12 @@
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
additional_queries:
|
||||
additional_queries:
|
||||
time: "SELECT * FROM time"
|
||||
macs: "SELECT mac FROM interface_details"
|
||||
detail_query_overrides:
|
||||
users:
|
||||
mdm: "SELECT enrolled, server_url, installed_from_dep, payload_identifier FROM mdm;"
|
||||
mdm: "SELECT enrolled, server_url, installed_from_dep, payload_identifier FROM mdm;"
|
||||
fleet_desktop:
|
||||
transparency_url: https://fleetdm.com/transparency
|
||||
host_expiry_settings:
|
||||
@@ -64,6 +64,7 @@ mdm:
|
||||
issuer_uri: https://some-mdm-issuer-uri.com
|
||||
metadata: some-mdm-metadata
|
||||
metadata_url: http://some-mdm-metadata-url.com
|
||||
end_user_license_agreement: ./lib/eula/test.pdf
|
||||
volume_purchasing_program:
|
||||
- location: Fleet Device Management Inc.
|
||||
teams:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
additional_queries:
|
||||
additional_queries:
|
||||
time: "SELECT * FROM time"
|
||||
macs: "SELECT mac FROM interface_details"
|
||||
detail_query_overrides:
|
||||
@@ -65,6 +65,7 @@ mdm:
|
||||
issuer_uri: https://some-mdm-issuer-uri.com
|
||||
metadata: ___GITOPS_COMMENT_6___
|
||||
metadata_url: ___GITOPS_COMMENT_7___
|
||||
end_user_license_agreement: ./lib/eula/test.pdf
|
||||
volume_purchasing_program:
|
||||
- location: Fleet Device Management Inc.
|
||||
teams:
|
||||
|
||||
@@ -99,6 +99,7 @@ org_settings:
|
||||
issuer_uri: https://some-mdm-issuer-uri.com
|
||||
metadata: # TODO: Add your MDM end user auth metadata here
|
||||
metadata_url: # TODO: Add your MDM end user auth metadata URL here
|
||||
end_user_license_agreement: ./lib/eula/test.pdf
|
||||
volume_purchasing_program:
|
||||
- location: Fleet Device Management Inc.
|
||||
teams:
|
||||
|
||||
@@ -411,6 +411,9 @@ func SetupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
|
||||
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
||||
return document, nil, nil
|
||||
}
|
||||
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
||||
return nil, ¬FoundError{} // No existing EULA
|
||||
}
|
||||
|
||||
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
|
||||
t.Setenv("ORG_NAME", orgName)
|
||||
|
||||
@@ -462,7 +462,7 @@ func (svc *Service) GetMDMAppleBootstrapPackageSummary(ctx context.Context, team
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func (svc *Service) MDMCreateEULA(ctx context.Context, name string, f io.ReadSeeker) error {
|
||||
func (svc *Service) MDMCreateEULA(ctx context.Context, name string, f io.ReadSeeker, dryRun bool) error {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMEULA{}, fleet.ActionWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -489,10 +489,18 @@ func (svc *Service) MDMCreateEULA(ctx context.Context, name string, f io.ReadSee
|
||||
return ctxerr.Wrap(ctx, err, "reading EULA bytes")
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
_, _ = hash.Write(bytes)
|
||||
|
||||
eula := &fleet.MDMEULA{
|
||||
Name: name,
|
||||
Token: uuid.New().String(),
|
||||
Bytes: bytes,
|
||||
Name: name,
|
||||
Token: uuid.New().String(),
|
||||
Sha256: hash.Sum(nil),
|
||||
Bytes: bytes,
|
||||
}
|
||||
|
||||
if err := svc.ds.MDMInsertEULA(ctx, eula); err != nil {
|
||||
@@ -510,11 +518,15 @@ func (svc *Service) MDMGetEULABytes(ctx context.Context, token string) (*fleet.M
|
||||
return svc.ds.MDMGetEULABytes(ctx, token)
|
||||
}
|
||||
|
||||
func (svc *Service) MDMDeleteEULA(ctx context.Context, token string) error {
|
||||
func (svc *Service) MDMDeleteEULA(ctx context.Context, token string, dryRun bool) error {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.MDMEULA{}, fleet.ActionWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := svc.ds.MDMDeleteEULA(ctx, token); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "deleting EULA")
|
||||
}
|
||||
|
||||
@@ -906,6 +906,20 @@ allow {
|
||||
action == write
|
||||
}
|
||||
|
||||
# Global admins can read, write, and list MDM apple eula information.
|
||||
allow {
|
||||
object.type == "mdm_apple_eula"
|
||||
subject.global_role == admin
|
||||
action == [read, write, list][_]
|
||||
}
|
||||
|
||||
# Global gitops can read and write the EULA.
|
||||
allow {
|
||||
object.type == "mdm_apple_eula"
|
||||
subject.global_role == gitops
|
||||
action == [read, write][_]
|
||||
}
|
||||
|
||||
##
|
||||
# MDM Apple Setup Assistant
|
||||
##
|
||||
|
||||
@@ -2398,3 +2398,43 @@ func TestHostHealth(t *testing.T) {
|
||||
{user: test.UserTeamMaintainerTeam2, object: hostHealth, action: read, allow: false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMDMAppleEULA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
eula := &fleet.MDMEULA{}
|
||||
runTestCases(t, []authTestCase{
|
||||
{user: nil, object: eula, action: read, allow: false},
|
||||
{user: test.UserGitOps, object: eula, action: read, allow: true},
|
||||
{user: test.UserGitOps, object: eula, action: write, allow: true},
|
||||
|
||||
{user: test.UserTeamGitOpsTeam1, object: eula, action: read, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam1, object: eula, action: write, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam2, object: eula, action: read, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam2, object: eula, action: write, allow: false},
|
||||
|
||||
{user: test.UserAdmin, object: eula, action: read, allow: true},
|
||||
{user: test.UserAdmin, object: eula, action: write, allow: true},
|
||||
|
||||
{user: test.UserTeamAdminTeam1, object: eula, action: read, allow: false},
|
||||
{user: test.UserTeamAdminTeam1, object: eula, action: write, allow: false},
|
||||
{user: test.UserTeamAdminTeam2, object: eula, action: read, allow: false},
|
||||
{user: test.UserTeamAdminTeam2, object: eula, action: write, allow: false},
|
||||
|
||||
{user: test.UserObserver, object: eula, action: read, allow: false},
|
||||
{user: test.UserObserver, object: eula, action: write, allow: false},
|
||||
|
||||
{user: test.UserTeamObserverTeam1, object: eula, action: read, allow: false},
|
||||
{user: test.UserTeamObserverTeam1, object: eula, action: write, allow: false},
|
||||
{user: test.UserTeamObserverTeam2, object: eula, action: read, allow: false},
|
||||
{user: test.UserTeamObserverTeam2, object: eula, action: write, allow: false},
|
||||
|
||||
{user: test.UserMaintainer, object: eula, action: read, allow: false},
|
||||
{user: test.UserMaintainer, object: eula, action: write, allow: false},
|
||||
|
||||
{user: test.UserTeamMaintainerTeam1, object: eula, action: read, allow: false},
|
||||
{user: test.UserTeamMaintainerTeam1, object: eula, action: write, allow: false},
|
||||
{user: test.UserTeamMaintainerTeam2, object: eula, action: read, allow: false},
|
||||
{user: test.UserTeamMaintainerTeam2, object: eula, action: write, allow: false},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1274,7 +1274,7 @@ func batchSetProfileLabelAssociationsDB(
|
||||
func (ds *Datastore) MDMGetEULAMetadata(ctx context.Context) (*fleet.MDMEULA, error) {
|
||||
// Currently, there can only be one EULA in the database, and we're
|
||||
// hardcoding it's id to be 1 in order to enforce this restriction.
|
||||
stmt := "SELECT name, created_at, token FROM eulas WHERE id = 1"
|
||||
stmt := "SELECT name, created_at, token, sha256 FROM eulas WHERE id = 1"
|
||||
var eula fleet.MDMEULA
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &eula, stmt); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -1301,11 +1301,11 @@ func (ds *Datastore) MDMInsertEULA(ctx context.Context, eula *fleet.MDMEULA) err
|
||||
// We're intentionally hardcoding the id to be 1 because we only want to
|
||||
// allow one EULA.
|
||||
stmt := `
|
||||
INSERT INTO eulas (id, name, bytes, token)
|
||||
VALUES (1, ?, ?, ?)
|
||||
INSERT INTO eulas (id, name, bytes, token, sha256)
|
||||
VALUES (1, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, stmt, eula.Name, eula.Bytes, eula.Token)
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, stmt, eula.Name, eula.Bytes, eula.Token, eula.Sha256)
|
||||
if err != nil {
|
||||
if IsDuplicate(err) {
|
||||
return ctxerr.Wrap(ctx, alreadyExists("MDMEULA", eula.Token))
|
||||
|
||||
@@ -6756,9 +6756,10 @@ func testBatchSetMDMProfilesTransactionError(t *testing.T, ds *Datastore) {
|
||||
func testMDMEULA(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
eula := &fleet.MDMEULA{
|
||||
Token: uuid.New().String(),
|
||||
Name: "eula.pdf",
|
||||
Bytes: []byte("contents"),
|
||||
Token: uuid.New().String(),
|
||||
Name: "eula.pdf",
|
||||
Bytes: []byte("contents"),
|
||||
Sha256: []byte("test-sha256"),
|
||||
}
|
||||
|
||||
err := ds.MDMInsertEULA(ctx, eula)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jmoiron/sqlx/reflectx"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20250701155654, Down_20250701155654)
|
||||
}
|
||||
|
||||
func Up_20250701155654(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE eulas ADD COLUMN sha256 binary(32)")
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding sha256 to eulas table: %w", err)
|
||||
}
|
||||
|
||||
txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)}
|
||||
|
||||
type eula struct {
|
||||
ID int `db:"id"`
|
||||
Bytes []byte `db:"bytes"`
|
||||
}
|
||||
|
||||
var eulas []eula
|
||||
err = txx.Select(&eulas, "SELECT id, bytes FROM eulas")
|
||||
if err != nil {
|
||||
return fmt.Errorf("selecting existing eulas: %w", err)
|
||||
}
|
||||
|
||||
for _, e := range eulas {
|
||||
hash := sha256.New()
|
||||
_, _ = hash.Write(e.Bytes)
|
||||
sha256Hash := hash.Sum(nil)
|
||||
_, err = txx.Exec("UPDATE eulas SET sha256 = ? WHERE id = ?", sha256Hash, e.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating eula %d with sha256: %w", e.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20250701155654(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20250701155654(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
eulaBytes := []byte("test eula content")
|
||||
|
||||
hash := sha256.New()
|
||||
_, _ = hash.Write(eulaBytes)
|
||||
sha256 := hash.Sum(nil)
|
||||
|
||||
execNoErr(t, db,
|
||||
`INSERT INTO eulas (id, bytes, token, name) VALUES (?, ?, ?, ?)`,
|
||||
1, eulaBytes, "test-token", "test-name",
|
||||
)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
var got []byte
|
||||
err := db.Get(&got, `SELECT sha256 FROM eulas WHERE id = ?`, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, bytes.Equal(got, sha256))
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+2
-1
@@ -169,12 +169,13 @@ func (bp *MDMAppleBootstrapPackage) URL(host string) (string, error) {
|
||||
type MDMEULA struct {
|
||||
Name string `json:"name"`
|
||||
Bytes []byte `json:"bytes"`
|
||||
Sha256 []byte `json:"sha256" db:"sha256"`
|
||||
Token string `json:"token"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
func (e MDMEULA) AuthzType() string {
|
||||
return "mdm_apple"
|
||||
return "mdm_apple_eula"
|
||||
}
|
||||
|
||||
// ExpectedMDMProfile represents an MDM profile that is expected to be installed on a host.
|
||||
|
||||
@@ -985,9 +985,9 @@ type Service interface {
|
||||
// be used by clients to display information.
|
||||
MDMGetEULAMetadata(ctx context.Context) (*MDMEULA, error)
|
||||
// MDMCreateEULA adds a new EULA file.
|
||||
MDMCreateEULA(ctx context.Context, name string, file io.ReadSeeker) error
|
||||
MDMCreateEULA(ctx context.Context, name string, file io.ReadSeeker, dryRun bool) error
|
||||
// MDMAppleDelete EULA removes an EULA entry.
|
||||
MDMDeleteEULA(ctx context.Context, token string) error
|
||||
MDMDeleteEULA(ctx context.Context, token string, dryRun bool) error
|
||||
|
||||
// Create or update the MDM Apple Setup Assistant for a team or no team.
|
||||
SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst *MDMAppleSetupAssistant) (*MDMAppleSetupAssistant, error)
|
||||
|
||||
@@ -292,19 +292,27 @@ func TestAppleMDMAuthorization(t *testing.T) {
|
||||
checkAuthErr(t, err, shouldFailWithAuth)
|
||||
_, err = svc.ListMDMAppleDevices(ctx)
|
||||
checkAuthErr(t, err, shouldFailWithAuth)
|
||||
}
|
||||
|
||||
// check EULA routes
|
||||
_, err = svc.MDMGetEULAMetadata(ctx)
|
||||
// some eula methods read and write access for gitops users. We test them separately
|
||||
// from the other MDM methods.
|
||||
testEULAMethods := func(t *testing.T, user *fleet.User, shouldFailWithAuth bool) {
|
||||
ctx := test.UserContext(ctx, user)
|
||||
_, err := svc.MDMGetEULAMetadata(ctx)
|
||||
checkAuthErr(t, err, shouldFailWithAuth)
|
||||
err = svc.MDMCreateEULA(ctx, "eula.pdf", bytes.NewReader([]byte("%PDF-")))
|
||||
err = svc.MDMCreateEULA(ctx, "eula.pdf", bytes.NewReader([]byte("%PDF-")), false)
|
||||
checkAuthErr(t, err, shouldFailWithAuth)
|
||||
err = svc.MDMDeleteEULA(ctx, "foo")
|
||||
err = svc.MDMDeleteEULA(ctx, "foo", false)
|
||||
checkAuthErr(t, err, shouldFailWithAuth)
|
||||
}
|
||||
|
||||
// Only global admins can access the endpoints.
|
||||
testAuthdMethods(t, test.UserAdmin, false)
|
||||
|
||||
// Global admin and gitops users can access the eula endpoints.
|
||||
testEULAMethods(t, test.UserAdmin, false)
|
||||
testEULAMethods(t, test.UserGitOps, false)
|
||||
|
||||
// All other users should not have access to the endpoints.
|
||||
for _, user := range []*fleet.User{
|
||||
test.UserNoRoles,
|
||||
@@ -314,6 +322,7 @@ func TestAppleMDMAuthorization(t *testing.T) {
|
||||
test.UserTeamAdminTeam1,
|
||||
} {
|
||||
testAuthdMethods(t, user, true)
|
||||
testEULAMethods(t, user, true)
|
||||
}
|
||||
// Token authenticated endpoints can be accessed by anyone.
|
||||
ctx = test.UserContext(ctx, test.UserNoRoles)
|
||||
|
||||
@@ -1623,6 +1623,7 @@ func (c *Client) DoGitOps(
|
||||
group.EnrollSecret = &fleet.EnrollSecretSpec{Secrets: config.OrgSettings["secrets"].([]*fleet.EnrollSecret)}
|
||||
group.AppConfig.(map[string]interface{})["agent_options"] = config.AgentOptions
|
||||
delete(config.OrgSettings, "secrets") // secrets are applied separately in Client.ApplyGroup
|
||||
var eulaPath string
|
||||
|
||||
// Labels
|
||||
if config.Labels == nil || len(config.Labels) > 0 {
|
||||
@@ -1796,7 +1797,24 @@ func (c *Client) DoGitOps(
|
||||
WindowsEnabledAndConfigured: optjson.SetBool(windowsEnabledAndConfiguredAssumption),
|
||||
}
|
||||
}
|
||||
// check for the eula in the mdmAppConfig. If it exists we want to delete it
|
||||
// from the app config so it will not be applied to the group/team though the
|
||||
// ApplyGroup method. It will be applied separately.
|
||||
if endUserLicenseAgreement, ok := mdmAppConfig["end_user_license_agreement"].(string); ok && len(endUserLicenseAgreement) > 0 {
|
||||
eulaPath = endUserLicenseAgreement
|
||||
delete(mdmAppConfig, "end_user_license_agreement")
|
||||
}
|
||||
|
||||
group.AppConfig.(map[string]interface{})["scripts"] = scripts
|
||||
|
||||
// we want to apply the EULA only for the global settings
|
||||
if appConfig.License.IsPremium() && appConfig.MDM.EnabledAndConfigured {
|
||||
err = c.doGitOpsEULA(eulaPath, logFn, dryRun)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
} else if !config.IsNoTeam() {
|
||||
team = make(map[string]interface{})
|
||||
team["name"] = *config.TeamName
|
||||
@@ -2437,6 +2455,28 @@ func (c *Client) doGitOpsQueries(config *spec.GitOps, logFn func(format string,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) doGitOpsEULA(eulaPath string, logFn func(format string, args ...interface{}), dryRun bool) error {
|
||||
if eulaPath == "" {
|
||||
err := c.DeleteEULAIfNeeded(dryRun)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting EULA: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := c.UploadEULAIfNeeded(eulaPath, dryRun)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error uploading EULA: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
logFn("[+] would've applied EULA\n")
|
||||
} else {
|
||||
logFn("[+] applied EULA\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetGitOpsSecrets(
|
||||
config *spec.GitOps,
|
||||
) []string {
|
||||
|
||||
@@ -410,3 +410,142 @@ func (c *Client) MDMWipeHost(hostID uint) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type eulaContent struct {
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
// eulaContent implements the bodyHandler interface so that we can read the
|
||||
// response body directly into a byte slice which represents the EULA file content.
|
||||
// This handler will be called in the Client.parseResponse method.
|
||||
func (ec *eulaContent) Handle(res *http.Response) error {
|
||||
b, err := io.ReadAll(res.Body)
|
||||
ec.Bytes = b
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) GetEULAContent(token string) ([]byte, error) {
|
||||
verb, path := "GET", fmt.Sprintf("/api/latest/fleet/setup_experience/eula/%s", token)
|
||||
request := getMDMEULARequest{}
|
||||
var responseBody eulaContent
|
||||
err := c.authenticatedRequest(request, verb, path, &responseBody)
|
||||
return responseBody.Bytes, err
|
||||
}
|
||||
|
||||
func (c *Client) GetEULAMetadata() (*fleet.MDMEULA, error) {
|
||||
verb, path := "GET", "/api/latest/fleet/setup_experience/eula/metadata"
|
||||
request := getMDMEULAMetadataRequest{}
|
||||
var responseBody getMDMEULAMetadataResponse
|
||||
err := c.authenticatedRequest(request, verb, path, &responseBody)
|
||||
return responseBody.MDMEULA, err
|
||||
}
|
||||
|
||||
func (c *Client) DeleteEULAIfNeeded(dryRun bool) error {
|
||||
eula, err := c.GetEULAMetadata()
|
||||
switch {
|
||||
case errors.As(err, ¬FoundErr{}):
|
||||
// not found is OK, it means there is nothing to delete
|
||||
return nil
|
||||
case err != nil:
|
||||
return fmt.Errorf("getting eula metadata: %w", err)
|
||||
}
|
||||
|
||||
err = c.DeleteEULA(eula.Token, dryRun)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting eula: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteEULA(token string, dryRun bool) error {
|
||||
verb, path := "DELETE", fmt.Sprintf("/api/latest/fleet/setup_experience/eula/%s", token)
|
||||
request := deleteMDMEULARequest{}
|
||||
var responseBody deleteMDMEULAResponse
|
||||
err := c.authenticatedRequestWithQuery(request, verb, path, &responseBody, fmt.Sprintf("dry_run=%t", dryRun))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) UploadEULAIfNeeded(eulaPath string, dryRun bool) error {
|
||||
isFirstTime := false
|
||||
oldMeta, err := c.GetEULAMetadata()
|
||||
if err != nil {
|
||||
// not found is OK, it means this is our first time uploading a eula
|
||||
if !errors.As(err, ¬FoundErr{}) {
|
||||
return fmt.Errorf("getting eula metadata: %w", err)
|
||||
}
|
||||
isFirstTime = true
|
||||
}
|
||||
|
||||
// read file to get the new file bytes
|
||||
eulaBytes, err := os.ReadFile(eulaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading eula file: %w", err)
|
||||
}
|
||||
|
||||
if !isFirstTime {
|
||||
newChecksum := sha256.Sum256(eulaBytes)
|
||||
|
||||
// compare checksums, if they're equal then we can skip the eula upload
|
||||
if bytes.Equal(oldMeta.Sha256, newChecksum[:]) && oldMeta.Name == filepath.Base(eulaPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// similar to the expected UI experience, delete the old eula first
|
||||
err = c.DeleteEULA(oldMeta.Token, dryRun)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting old eula: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.UploadEULA(eulaPath, dryRun); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) UploadEULA(eulaPath string, dryRun bool) error {
|
||||
verb, path := "POST", "/api/latest/fleet/setup_experience/eula"
|
||||
|
||||
var b bytes.Buffer
|
||||
w := multipart.NewWriter(&b)
|
||||
|
||||
// add the eula field
|
||||
fw, err := w.CreateFormFile("eula", filepath.Base(eulaPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(eulaPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := io.Copy(fw, file); err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing writer: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.doContextWithBodyAndHeaders(context.Background(), verb, path, fmt.Sprintf("dry_run=%t", dryRun),
|
||||
b.Bytes(),
|
||||
map[string]string{
|
||||
"Content-Type": w.FormDataContentType(),
|
||||
"Accept": "application/json",
|
||||
"Authorization": fmt.Sprintf("Bearer %s", c.token),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("do multipart request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var eulaResponse createMDMEULAResponse
|
||||
if err := c.parseResponse(verb, path, resp, &eulaResponse); err != nil {
|
||||
return fmt.Errorf("parse response: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"database/sql"
|
||||
@@ -3657,6 +3658,7 @@ func (s *integrationMDMTestSuite) TestEULA() {
|
||||
t := s.T()
|
||||
pdfBytes := []byte("%PDF-1.pdf-contents")
|
||||
pdfName := "eula.pdf"
|
||||
pdfHash := sha256.Sum256(pdfBytes)
|
||||
|
||||
// trying to get metadata about an EULA that hasn't been uploaded yet is an error
|
||||
metadataResp := getMDMEULAMetadataResponse{}
|
||||
@@ -3676,6 +3678,7 @@ func (s *integrationMDMTestSuite) TestEULA() {
|
||||
require.NotEmpty(t, metadataResp.MDMEULA.Token)
|
||||
require.NotEmpty(t, metadataResp.MDMEULA.CreatedAt)
|
||||
require.Equal(t, pdfName, metadataResp.MDMEULA.Name)
|
||||
require.Equal(t, pdfHash[:], metadataResp.MDMEULA.Sha256)
|
||||
eulaToken := metadataResp.Token
|
||||
|
||||
// download EULA
|
||||
|
||||
+14
-7
@@ -241,7 +241,8 @@ func (svc *Service) VerifyMDMAppleConfigured(ctx context.Context) error {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type createMDMEULARequest struct {
|
||||
EULA *multipart.FileHeader
|
||||
EULA *multipart.FileHeader
|
||||
DryRun bool `query:"dry_run,optional"` // if true, apply validation but do not save changes
|
||||
}
|
||||
|
||||
// TODO: We parse the whole body before running svc.authz.Authorize.
|
||||
@@ -262,8 +263,13 @@ func (createMDMEULARequest) DecodeRequest(ctx context.Context, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
dryRun := false
|
||||
if v := r.URL.Query().Get("dry_run"); v != "" {
|
||||
dryRun, _ = strconv.ParseBool(v)
|
||||
}
|
||||
return &createMDMEULARequest{
|
||||
EULA: r.MultipartForm.File["eula"][0],
|
||||
EULA: r.MultipartForm.File["eula"][0],
|
||||
DryRun: dryRun,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -281,14 +287,14 @@ func createMDMEULAEndpoint(ctx context.Context, request interface{}, svc fleet.S
|
||||
}
|
||||
defer ff.Close()
|
||||
|
||||
if err := svc.MDMCreateEULA(ctx, req.EULA.Filename, ff); err != nil {
|
||||
if err := svc.MDMCreateEULA(ctx, req.EULA.Filename, ff, req.DryRun); err != nil {
|
||||
return createMDMEULAResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return createMDMEULAResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) MDMCreateEULA(ctx context.Context, name string, file io.ReadSeeker) error {
|
||||
func (svc *Service) MDMCreateEULA(ctx context.Context, name string, file io.ReadSeeker, dryRun bool) error {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
@@ -381,7 +387,8 @@ func (svc *Service) MDMGetEULAMetadata(ctx context.Context) (*fleet.MDMEULA, err
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type deleteMDMEULARequest struct {
|
||||
Token string `url:"token"`
|
||||
Token string `url:"token"`
|
||||
DryRun bool `query:"dry_run,optional"` // if true, apply validation but do not delete
|
||||
}
|
||||
|
||||
type deleteMDMEULAResponse struct {
|
||||
@@ -392,13 +399,13 @@ func (r deleteMDMEULAResponse) Error() error { return r.Err }
|
||||
|
||||
func deleteMDMEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*deleteMDMEULARequest)
|
||||
if err := svc.MDMDeleteEULA(ctx, req.Token); err != nil {
|
||||
if err := svc.MDMDeleteEULA(ctx, req.Token, req.DryRun); err != nil {
|
||||
return deleteMDMEULAResponse{Err: err}, nil
|
||||
}
|
||||
return deleteMDMEULAResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) MDMDeleteEULA(ctx context.Context, token string) error {
|
||||
func (svc *Service) MDMDeleteEULA(ctx context.Context, token string, dryRun bool) error {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
Reference in New Issue
Block a user