Add new fleet_desktop property to config object (#6151)

This commit is contained in:
gillespi314 2022-06-10 10:39:02 -05:00 committed by GitHub
parent 7c756bcd44
commit a3ab5646f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 395 additions and 13 deletions

View File

@ -0,0 +1,4 @@
- Add `fleet_desktop.transparency_url` to `app_config_json`
- Set default `transparency_url="https://fleetdm.com/transparency`
- Enable Fleet Premium licensees to set custom `transparency_url` via REST API and `fleetctl apply`
- Add `transparency_url` to `GET /device/{token}` endpoint response

View File

@ -440,6 +440,8 @@ func TestGetConfig(t *testing.T) {
apiVersion: v1 apiVersion: v1
kind: config kind: config
spec: spec:
fleet_desktop:
transparency_url: https://fleetdm.com/transparency
host_expiry_settings: host_expiry_settings:
host_expiry_enabled: false host_expiry_enabled: false
host_expiry_window: 0 host_expiry_window: 0
@ -499,7 +501,7 @@ spec:
enable_vulnerabilities_webhook: false enable_vulnerabilities_webhook: false
host_batch_size: 0 host_batch_size: 0
` `
expectedJson := `{"kind":"config","apiVersion":"v1","spec":{"org_info":{"org_name":"","org_logo_url":""},"server_settings":{"server_url":"","live_query_disabled":false,"enable_analytics":false,"deferred_save_host":false},"smtp_settings":{"enable_smtp":false,"configured":false,"sender_address":"","server":"","port":0,"authentication_type":"","user_name":"","password":"","enable_ssl_tls":false,"authentication_method":"","domain":"","verify_ssl_certs":false,"enable_start_tls":false},"host_expiry_settings":{"host_expiry_enabled":false,"host_expiry_window":0},"host_settings":{"enable_host_users":true,"enable_software_inventory":false},"sso_settings":{"entity_id":"","issuer_uri":"","idp_image_url":"","metadata":"","metadata_url":"","idp_name":"","enable_sso":false,"enable_sso_idp_login":false},"vulnerability_settings":{"databases_path":"/some/path"},"webhook_settings":{"host_status_webhook":{"enable_host_status_webhook":false,"destination_url":"","host_percentage":0,"days_count":0},"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0},"vulnerabilities_webhook":{"enable_vulnerabilities_webhook":false,"destination_url":"","host_batch_size":0},"interval":"0s"},"integrations":{"jira":null,"zendesk":null}}} expectedJson := `{"kind":"config","apiVersion":"v1","spec":{"org_info":{"org_name":"","org_logo_url":""},"server_settings":{"server_url":"","live_query_disabled":false,"enable_analytics":false,"deferred_save_host":false},"smtp_settings":{"enable_smtp":false,"configured":false,"sender_address":"","server":"","port":0,"authentication_type":"","user_name":"","password":"","enable_ssl_tls":false,"authentication_method":"","domain":"","verify_ssl_certs":false,"enable_start_tls":false},"host_expiry_settings":{"host_expiry_enabled":false,"host_expiry_window":0},"host_settings":{"enable_host_users":true,"enable_software_inventory":false},"sso_settings":{"entity_id":"","issuer_uri":"","idp_image_url":"","metadata":"","metadata_url":"","idp_name":"","enable_sso":false,"enable_sso_idp_login":false},"fleet_desktop":{"transparency_url":"https://fleetdm.com/transparency"},"vulnerability_settings":{"databases_path":"/some/path"},"webhook_settings":{"host_status_webhook":{"enable_host_status_webhook":false,"destination_url":"","host_percentage":0,"days_count":0},"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0},"vulnerabilities_webhook":{"enable_vulnerabilities_webhook":false,"destination_url":"","host_batch_size":0},"interval":"0s"},"integrations":{"jira":null,"zendesk":null}}}
` `
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "config"})) assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "config"}))
@ -512,6 +514,8 @@ spec:
apiVersion: v1 apiVersion: v1
kind: config kind: config
spec: spec:
fleet_desktop:
transparency_url: https://fleetdm.com/transparency
host_expiry_settings: host_expiry_settings:
host_expiry_enabled: false host_expiry_enabled: false
host_expiry_window: 0 host_expiry_window: 0
@ -602,7 +606,7 @@ spec:
enable_vulnerabilities_webhook: false enable_vulnerabilities_webhook: false
host_batch_size: 0 host_batch_size: 0
` `
expectedJson := `{"kind":"config","apiVersion":"v1","spec":{"org_info":{"org_name":"","org_logo_url":""},"server_settings":{"server_url":"","live_query_disabled":false,"enable_analytics":false,"deferred_save_host":false},"smtp_settings":{"enable_smtp":false,"configured":false,"sender_address":"","server":"","port":0,"authentication_type":"","user_name":"","password":"","enable_ssl_tls":false,"authentication_method":"","domain":"","verify_ssl_certs":false,"enable_start_tls":false},"host_expiry_settings":{"host_expiry_enabled":false,"host_expiry_window":0},"host_settings":{"enable_host_users":true,"enable_software_inventory":false},"sso_settings":{"entity_id":"","issuer_uri":"","idp_image_url":"","metadata":"","metadata_url":"","idp_name":"","enable_sso":false,"enable_sso_idp_login":false},"vulnerability_settings":{"databases_path":"/some/path"},"webhook_settings":{"host_status_webhook":{"enable_host_status_webhook":false,"destination_url":"","host_percentage":0,"days_count":0},"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0},"vulnerabilities_webhook":{"enable_vulnerabilities_webhook":false,"destination_url":"","host_batch_size":0},"interval":"0s"},"integrations":{"jira":null,"zendesk":null},"update_interval":{"osquery_detail":"1h0m0s","osquery_policy":"1h0m0s"},"vulnerabilities":{"databases_path":"","periodicity":"0s","cpe_database_url":"","cve_feed_prefix_url":"","current_instance_checks":"","disable_data_sync":false,"recent_vulnerability_max_age":"0s"},"license":{"tier":"free","expiration":"0001-01-01T00:00:00Z"},"logging":{"debug":true,"json":false,"result":{"plugin":"filesystem","config":{"enable_log_compression":false,"enable_log_rotation":false,"result_log_file":"/dev/null","status_log_file":"/dev/null"}},"status":{"plugin":"filesystem","config":{"enable_log_compression":false,"enable_log_rotation":false,"result_log_file":"/dev/null","status_log_file":"/dev/null"}}}}} expectedJson := `{"kind":"config","apiVersion":"v1","spec":{"org_info":{"org_name":"","org_logo_url":""},"server_settings":{"server_url":"","live_query_disabled":false,"enable_analytics":false,"deferred_save_host":false},"smtp_settings":{"enable_smtp":false,"configured":false,"sender_address":"","server":"","port":0,"authentication_type":"","user_name":"","password":"","enable_ssl_tls":false,"authentication_method":"","domain":"","verify_ssl_certs":false,"enable_start_tls":false},"host_expiry_settings":{"host_expiry_enabled":false,"host_expiry_window":0},"host_settings":{"enable_host_users":true,"enable_software_inventory":false},"sso_settings":{"entity_id":"","issuer_uri":"","idp_image_url":"","metadata":"","metadata_url":"","idp_name":"","enable_sso":false,"enable_sso_idp_login":false},"fleet_desktop":{"transparency_url":"https://fleetdm.com/transparency"},"vulnerability_settings":{"databases_path":"/some/path"},"webhook_settings":{"host_status_webhook":{"enable_host_status_webhook":false,"destination_url":"","host_percentage":0,"days_count":0},"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0},"vulnerabilities_webhook":{"enable_vulnerabilities_webhook":false,"destination_url":"","host_batch_size":0},"interval":"0s"},"integrations":{"jira":null,"zendesk":null},"update_interval":{"osquery_detail":"1h0m0s","osquery_policy":"1h0m0s"},"vulnerabilities":{"databases_path":"","periodicity":"0s","cpe_database_url":"","cve_feed_prefix_url":"","current_instance_checks":"","disable_data_sync":false,"recent_vulnerability_max_age":"0s"},"license":{"tier":"free","expiration":"0001-01-01T00:00:00Z"},"logging":{"debug":true,"json":false,"result":{"plugin":"filesystem","config":{"enable_log_compression":false,"enable_log_rotation":false,"result_log_file":"/dev/null","status_log_file":"/dev/null"}},"status":{"plugin":"filesystem","config":{"enable_log_compression":false,"enable_log_rotation":false,"result_log_file":"/dev/null","status_log_file":"/dev/null"}}}}}
` `
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "config", "--include-server-config"})) assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "config", "--include-server-config"}))

View File

@ -1014,6 +1014,7 @@ Modifies the Fleet's configuration with the supplied information.
| host_expiry_enabled | boolean | body | _Host expiry settings_. When enabled, allows automatic cleanup of hosts that have not communicated with Fleet in some number of days. | | host_expiry_enabled | boolean | body | _Host expiry settings_. When enabled, allows automatic cleanup of hosts that have not communicated with Fleet in some number of days. |
| host_expiry_window | integer | body | _Host expiry settings_. If a host has not communicated with Fleet in the specified number of days, it will be removed. | | host_expiry_window | integer | body | _Host expiry settings_. If a host has not communicated with Fleet in the specified number of days, it will be removed. |
| agent_options | objects | body | The agent_options spec that is applied to all hosts. In Fleet 4.0.0 the `api/v1/fleet/spec/osquery_options` endpoints were removed. | | agent_options | objects | body | The agent_options spec that is applied to all hosts. In Fleet 4.0.0 the `api/v1/fleet/spec/osquery_options` endpoints were removed. |
| transparency_url | string | body | _Fleet Desktop_. The URL used to display transparency information to users of Fleet Desktop. **Requires Fleet Premium license** |
| enable_host_status_webhook | boolean | body | _webhook_settings.host_status_webhook settings_. Whether or not the host status webhook is enabled. | | enable_host_status_webhook | boolean | body | _webhook_settings.host_status_webhook settings_. Whether or not the host status webhook is enabled. |
| destination_url | string | body | _webhook_settings.host_status_webhook settings_. The URL to deliver the webhook request to. | | destination_url | string | body | _webhook_settings.host_status_webhook settings_. The URL to deliver the webhook request to. |
| host_percentage | integer | body | _webhook_settings.host_status_webhook settings_. The minimum percentage of hosts that must fail to check in to Fleet in order to trigger the webhook request. | | host_percentage | integer | body | _webhook_settings.host_status_webhook settings_. The minimum percentage of hosts that must fail to check in to Fleet in order to trigger the webhook request. |

View File

@ -0,0 +1,30 @@
package tables
import (
"database/sql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/pkg/errors"
)
func init() {
MigrationClient.AddMigration(Up_20220608113128, Down_20220608113128)
}
func Up_20220608113128(tx *sql.Tx) error {
err := updateAppConfigJSON(tx, func(config *fleet.AppConfig) error {
if config.FleetDesktop.TransparencyURL != "" {
return errors.New("unexpected transparency_url value in app_config_json")
}
return nil
})
if err != nil {
return err
}
return nil
}
func Down_20220608113128(tx *sql.Tx) error {
return nil
}

View File

@ -0,0 +1,33 @@
package tables
import (
"encoding/json"
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/require"
)
func TestUp_20220608113128(t *testing.T) {
db := applyUpToPrev(t)
var prevRaw []byte
var prevConfig fleet.AppConfig
err := db.Get(&prevRaw, `SELECT json_value FROM app_config_json`)
require.NoError(t, err)
err = json.Unmarshal(prevRaw, &prevConfig)
require.NoError(t, err)
require.Empty(t, prevConfig.FleetDesktop.TransparencyURL)
applyNext(t, db)
var newRaw []byte
var newConfig fleet.AppConfig
err = db.Get(&newRaw, `SELECT json_value FROM app_config_json`)
require.NoError(t, err)
err = json.Unmarshal(newRaw, &newConfig)
require.NoError(t, err)
require.Equal(t, "", newConfig.FleetDesktop.TransparencyURL)
}

View File

@ -2,13 +2,14 @@ package tables
import ( import (
"database/sql" "database/sql"
"encoding/json"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/goose" "github.com/fleetdm/goose"
"github.com/pkg/errors"
) )
var ( var MigrationClient = goose.New("migration_status_tables", goose.MySqlDialect{})
MigrationClient = goose.New("migration_status_tables", goose.MySqlDialect{})
)
func columnExists(tx *sql.Tx, table, column string) bool { func columnExists(tx *sql.Tx, table, column string) bool {
var count int var count int
@ -31,3 +32,37 @@ WHERE
return count > 0 return count > 0
} }
// updateAppConfigJSON updates the `json_value` stored in the `app_config_json` after applying the
// supplied callback to the current config object.
func updateAppConfigJSON(tx *sql.Tx, fn func(config *fleet.AppConfig) error) error {
var raw []byte
row := tx.QueryRow(`SELECT json_value FROM app_config_json LIMIT 1`)
if err := row.Scan(&raw); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return errors.Wrap(err, "select app_config_json")
}
var config fleet.AppConfig
if err := json.Unmarshal(raw, &config); err != nil {
return errors.Wrap(err, "unmarshal app_config_json")
}
if err := fn(&config); err != nil {
return errors.Wrap(err, "callback app_config_json")
}
b, err := json.Marshal(config)
if err != nil {
return errors.Wrap(err, "marshal updated app_config_json")
}
const updateStmt = `UPDATE app_config_json SET json_value = ? WHERE id = 1`
if _, err := tx.Exec(updateStmt, b); err != nil {
return errors.Wrap(err, "update app_config_json")
}
return nil
}

View File

@ -36,7 +36,7 @@ CREATE TABLE `app_config_json` (
UNIQUE KEY `id` (`id`) UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO `app_config_json` VALUES (1,'{\"org_info\": {\"org_name\": \"\", \"org_logo_url\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_sso_idp_login\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"host_settings\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false}, \"webhook_settings\": {\"interval\": \"24h0m0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); INSERT INTO `app_config_json` VALUES (1,'{\"org_info\": {\"org_name\": \"\", \"org_logo_url\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_sso_idp_login\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"host_settings\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false}, \"webhook_settings\": {\"interval\": \"24h0m0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8 */;
CREATE TABLE `carve_blocks` ( CREATE TABLE `carve_blocks` (

View File

@ -114,6 +114,8 @@ type AppConfig struct {
SMTPTest bool `json:"smtp_test,omitempty"` SMTPTest bool `json:"smtp_test,omitempty"`
// SSOSettings is single sign on settings // SSOSettings is single sign on settings
SSOSettings SSOSettings `json:"sso_settings"` SSOSettings SSOSettings `json:"sso_settings"`
// FleetDesktop holds settings for Fleet Desktop that can be changed via the API.
FleetDesktop FleetDesktopSettings `json:"fleet_desktop"`
// VulnerabilitySettings defines how fleet will behave while scanning for vulnerabilities in the host software // VulnerabilitySettings defines how fleet will behave while scanning for vulnerabilities in the host software
VulnerabilitySettings VulnerabilitySettings `json:"vulnerability_settings"` VulnerabilitySettings VulnerabilitySettings `json:"vulnerability_settings"`
@ -261,6 +263,15 @@ type HostSettings struct {
AdditionalQueries *json.RawMessage `json:"additional_queries,omitempty"` AdditionalQueries *json.RawMessage `json:"additional_queries,omitempty"`
} }
// FleetDesktopSettings contains settings used to configure Fleet Desktop.
type FleetDesktopSettings struct {
// TransparencyURL is the URL used for the “Transparency” link in the Fleet Desktop menu.
TransparencyURL string `json:"transparency_url"`
}
// DefaultTransparencyURL is the default URL used for the “Transparency” link in the Fleet Desktop menu.
const DefaultTransparencyURL = "https://fleetdm.com/transparency"
type OrderDirection int type OrderDirection int
const ( const (

View File

@ -75,6 +75,14 @@ func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Se
hostExpirySettings = config.HostExpirySettings hostExpirySettings = config.HostExpirySettings
agentOptions = config.AgentOptions agentOptions = config.AgentOptions
} }
transparencyURL := fleet.DefaultTransparencyURL
// Fleet Premium license is required for custom transparency url
if license.Tier == "premium" && config.FleetDesktop.TransparencyURL != "" {
transparencyURL = config.FleetDesktop.TransparencyURL
}
fleetDesktop := fleet.FleetDesktopSettings{TransparencyURL: transparencyURL}
hostSettings := config.HostSettings hostSettings := config.HostSettings
response := appConfigResponse{ response := appConfigResponse{
AppConfig: fleet.AppConfig{ AppConfig: fleet.AppConfig{
@ -88,6 +96,8 @@ func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Se
HostExpirySettings: hostExpirySettings, HostExpirySettings: hostExpirySettings,
AgentOptions: agentOptions, AgentOptions: agentOptions,
FleetDesktop: fleetDesktop,
WebhookSettings: config.WebhookSettings, WebhookSettings: config.WebhookSettings,
Integrations: config.Integrations, Integrations: config.Integrations,
}, },
@ -157,6 +167,11 @@ func modifyAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet
if response.SMTPSettings.SMTPPassword != "" { if response.SMTPSettings.SMTPPassword != "" {
response.SMTPSettings.SMTPPassword = fleet.MaskedPassword response.SMTPSettings.SMTPPassword = fleet.MaskedPassword
} }
if license.Tier != "premium" || response.FleetDesktop.TransparencyURL == "" {
response.FleetDesktop.TransparencyURL = fleet.DefaultTransparencyURL
}
return response, nil return response, nil
} }
@ -172,6 +187,11 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte) (*fleet.AppCo
return nil, err return nil, err
} }
license, err := svc.License(ctx)
if err != nil {
return nil, err
}
oldSmtpSettings := appConfig.SMTPSettings oldSmtpSettings := appConfig.SMTPSettings
storedJiraByProjectKey, err := fleet.IndexJiraIntegrations(appConfig.Integrations.Jira) storedJiraByProjectKey, err := fleet.IndexJiraIntegrations(appConfig.Integrations.Jira)
@ -243,6 +263,19 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte) (*fleet.AppCo
} }
appConfig.Integrations.Zendesk = newAppConfig.Integrations.Zendesk appConfig.Integrations.Zendesk = newAppConfig.Integrations.Zendesk
transparencyURL := appConfig.FleetDesktop.TransparencyURL
if transparencyURL != "" && license.Tier != "premium" {
invalid.Append("transparency_url", ErrMissingLicense.Error())
return nil, ctxerr.Wrap(ctx, invalid)
}
if _, err := url.Parse(transparencyURL); err != nil {
invalid.Append("transparency_url", err.Error())
return nil, ctxerr.Wrap(ctx, invalid)
}
appConfig.FleetDesktop.TransparencyURL = transparencyURL
if err := svc.ds.SaveAppConfig(ctx, appConfig); err != nil { if err := svc.ds.SaveAppConfig(ctx, appConfig); err != nil {
return nil, err return nil, err
} }

View File

@ -389,3 +389,95 @@ func TestModifyAppConfigSMTPConfigured(t *testing.T) {
require.False(t, dsAppConfig.SMTPSettings.SMTPEnabled) require.False(t, dsAppConfig.SMTPSettings.SMTPEnabled)
require.False(t, dsAppConfig.SMTPSettings.SMTPConfigured) require.False(t, dsAppConfig.SMTPSettings.SMTPConfigured)
} }
// TestTransparencyURL tests that Fleet Premium licensees can use custom transparency urls and Fleet
// Free licensees are restricted to the default transparency url.
func TestTransparencyURL(t *testing.T) {
ds := new(mock.Store)
admin := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: admin})
checkLicenseErr := func(t *testing.T, shouldFail bool, err error) {
if shouldFail {
require.Error(t, err)
require.ErrorContains(t, err, "missing or invalid license")
} else {
require.NoError(t, err)
}
}
testCases := []struct {
name string
licenseTier string
initialURL string
newURL string
expectedURL string
shouldFailModify bool
}{
{
name: "customURL",
licenseTier: "free",
initialURL: "",
newURL: "customURL",
expectedURL: "",
shouldFailModify: true,
},
{
name: "customURL",
licenseTier: "premium",
initialURL: "",
newURL: "customURL",
expectedURL: "customURL",
shouldFailModify: false,
},
{
name: "emptyURL",
licenseTier: "free",
initialURL: "",
newURL: "",
expectedURL: "",
shouldFailModify: false,
},
{
name: "emptyURL",
licenseTier: "premium",
initialURL: "customURL",
newURL: "",
expectedURL: "",
shouldFailModify: false,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
svc := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: tt.licenseTier}})
dsAppConfig := &fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: tt.initialURL}}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return dsAppConfig, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, conf *fleet.AppConfig) error {
*dsAppConfig = *conf
return nil
}
ac, err := svc.AppConfig(ctx)
require.NoError(t, err)
require.Equal(t, tt.initialURL, ac.FleetDesktop.TransparencyURL)
raw, err := json.Marshal(fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: tt.newURL}})
require.NoError(t, err)
modified, err := svc.ModifyAppConfig(ctx, raw)
checkLicenseErr(t, tt.shouldFailModify, err)
if modified != nil {
require.Equal(t, tt.expectedURL, modified.FleetDesktop.TransparencyURL)
ac, err = svc.AppConfig(ctx)
require.NoError(t, err)
require.Equal(t, tt.expectedURL, ac.FleetDesktop.TransparencyURL)
}
})
}
}

View File

@ -21,10 +21,11 @@ func (r *getDeviceHostRequest) deviceAuthToken() string {
} }
type getDeviceHostResponse struct { type getDeviceHostResponse struct {
Host *HostDetailResponse `json:"host"` Host *HostDetailResponse `json:"host"`
OrgLogoURL string `json:"org_logo_url"` OrgLogoURL string `json:"org_logo_url"`
Err error `json:"error,omitempty"` TransparencyURL string `json:"transparency_url"`
License fleet.LicenseInfo `json:"license"` Err error `json:"error,omitempty"`
License fleet.LicenseInfo `json:"license"`
} }
func (r getDeviceHostResponse) error() error { return r.Err } func (r getDeviceHostResponse) error() error { return r.Err }
@ -64,10 +65,16 @@ func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.S
return getDeviceHostResponse{Err: err}, nil return getDeviceHostResponse{Err: err}, nil
} }
transparencyURL := fleet.DefaultTransparencyURL
if license.Tier == "premium" && ac.FleetDesktop.TransparencyURL != "" {
transparencyURL = ac.FleetDesktop.TransparencyURL
}
return getDeviceHostResponse{ return getDeviceHostResponse{
Host: resp, Host: resp,
OrgLogoURL: ac.OrgInfo.OrgLogoURL, OrgLogoURL: ac.OrgInfo.OrgLogoURL,
License: *license, TransparencyURL: transparencyURL,
License: *license,
}, nil }, nil
} }

View File

@ -4634,6 +4634,71 @@ func (s *integrationTestSuite) TestDeviceAuthenticatedEndpoints() {
require.NotNil(t, apiFeaturesResp.Features) require.NotNil(t, apiFeaturesResp.Features)
} }
// TestDefaultTransparencyURL tests that Fleet Free licensees are restricted to the default transparency url.
func (s *integrationTestSuite) TestDefaultTransparencyURL() {
t := s.T()
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: t.Name(),
NodeKey: t.Name(),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
Platform: "darwin",
})
require.NoError(t, err)
// create device token for host
token := "token_test_default_transparency_url"
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, host.ID, token)
return err
})
// confirm initial default url
acResp := appConfigResponse{}
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
require.NotNil(t, acResp)
require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL)
// confirm device endpoint returns initial default url
deviceResp := &getDeviceHostResponse{}
rawResp := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
json.NewDecoder(rawResp.Body).Decode(deviceResp)
rawResp.Body.Close()
require.NoError(t, deviceResp.Err)
require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL)
// empty string applies default url
acResp = appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: ""}}, http.StatusOK, &acResp)
require.NotNil(t, acResp)
require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL)
// device endpoint returns default url
deviceResp = &getDeviceHostResponse{}
rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
json.NewDecoder(rawResp.Body).Decode(deviceResp)
rawResp.Body.Close()
require.NoError(t, deviceResp.Err)
require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL)
// modify transparency url with custom url fails
acResp = appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: "customURL"}}, http.StatusUnprocessableEntity, &acResp)
// device endpoint still returns default url
deviceResp = &getDeviceHostResponse{}
rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
json.NewDecoder(rawResp.Body).Decode(deviceResp)
rawResp.Body.Close()
require.NoError(t, deviceResp.Err)
require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL)
}
func (s *integrationTestSuite) TestModifyUser() { func (s *integrationTestSuite) TestModifyUser() {
t := s.T() t := s.T()

View File

@ -1064,3 +1064,70 @@ func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() {
require.Equal(t, "http://example.com/logo", getDeviceHostResp.OrgLogoURL) require.Equal(t, "http://example.com/logo", getDeviceHostResp.OrgLogoURL)
require.Len(t, *getDeviceHostResp.Host.Policies, 2) require.Len(t, *getDeviceHostResp.Host.Policies, 2)
} }
// TestCustomTransparencyURL tests that Fleet Premium licensees can use custom transparency urls.
func (s *integrationEnterpriseTestSuite) TestCustomTransparencyURL() {
t := s.T()
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: t.Name(),
NodeKey: t.Name(),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
Platform: "darwin",
})
require.NoError(t, err)
// create device token for host
token := "token_test_custom_transparency_url"
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, host.ID, token)
return err
})
// confirm intitial default url
acResp := appConfigResponse{}
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
require.NotNil(t, acResp)
require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL)
// confirm device endpoint returns initial default url
deviceResp := &getDeviceHostResponse{}
rawResp := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
json.NewDecoder(rawResp.Body).Decode(deviceResp)
rawResp.Body.Close()
require.NoError(t, deviceResp.Err)
require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL)
// set custom url
acResp = appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: "customURL"}}, http.StatusOK, &acResp)
require.NotNil(t, acResp)
require.Equal(t, "customURL", acResp.FleetDesktop.TransparencyURL)
// device endpoint returns custom url
deviceResp = &getDeviceHostResponse{}
rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
json.NewDecoder(rawResp.Body).Decode(deviceResp)
rawResp.Body.Close()
require.NoError(t, deviceResp.Err)
require.Equal(t, "customURL", deviceResp.TransparencyURL)
// empty string applies default url
acResp = appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: ""}}, http.StatusOK, &acResp)
require.NotNil(t, acResp)
require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL)
// device endpoint returns default url
deviceResp = &getDeviceHostResponse{}
rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
json.NewDecoder(rawResp.Body).Decode(deviceResp)
rawResp.Body.Close()
require.NoError(t, deviceResp.Err)
require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL)
}