fleet/server/datastore/mysql/app_configs.go
Zach Wasserman 0a77f79d22
Backend and fleetctl for usage analytics (#1167)
- Add enable_analytics column to database.
- Allow enable_analytics to be set via API.
- Add messaging in fleetctl setup.

Note that this defaults to off for existing installations, and defaults
on for newly set up installs.

No collection or sending of analytics yet exists, we are strictly
storing the preference at this time.

Part of #454
2021-06-22 18:02:20 -07:00

253 lines
7.1 KiB
Go

package mysql
import (
"fmt"
"github.com/VividCortex/mysqlerr"
"github.com/fleetdm/fleet/server/fleet"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
func (d *Datastore) NewAppConfig(info *fleet.AppConfig) (*fleet.AppConfig, error) {
if err := d.SaveAppConfig(info); err != nil {
return nil, errors.Wrap(err, "new app config")
}
return info, nil
}
func (d *Datastore) AppConfig() (*fleet.AppConfig, error) {
info := &fleet.AppConfig{}
err := d.db.Get(info, "SELECT * FROM app_configs LIMIT 1")
if err != nil {
return nil, errors.Wrap(err, "selecting app config")
}
return info, nil
}
func (d *Datastore) isEventSchedulerEnabled() (bool, error) {
rows, err := d.db.Query("SELECT @@event_scheduler")
if err != nil {
return false, err
}
if !rows.Next() {
return false, errors.New("Error detecting MySQL event scheduler status.")
}
var value string
if err := rows.Scan(&value); err != nil {
return false, err
}
return value == "ON", nil
}
func (d *Datastore) ManageHostExpiryEvent(hostExpiryEnabled bool, hostExpiryWindow int) error {
var err error
hostExpiryConfig := struct {
Window int `db:"host_expiry_window"`
}{}
if err = d.db.Get(&hostExpiryConfig, "SELECT host_expiry_window from app_configs LIMIT 1"); err != nil {
return errors.Wrap(err, "get expiry window setting")
}
shouldUpdateWindow := hostExpiryEnabled && hostExpiryConfig.Window != hostExpiryWindow
if !hostExpiryEnabled || shouldUpdateWindow {
if _, err := d.db.Exec("DROP EVENT IF EXISTS host_expiry"); err != nil {
if driverErr, ok := err.(*mysql.MySQLError); !ok || driverErr.Number != mysqlerr.ER_DBACCESS_DENIED_ERROR {
return errors.Wrap(err, "drop existing host_expiry event")
}
}
}
if shouldUpdateWindow {
sql := fmt.Sprintf("CREATE EVENT IF NOT EXISTS host_expiry ON SCHEDULE EVERY 1 HOUR ON COMPLETION PRESERVE DO DELETE FROM hosts WHERE seen_time < DATE_SUB(NOW(), INTERVAL %d DAY)", hostExpiryWindow)
if _, err := d.db.Exec(sql); err != nil {
return errors.Wrap(err, "create new host_expiry event")
}
}
return nil
}
func (d *Datastore) SaveAppConfig(info *fleet.AppConfig) error {
eventSchedulerEnabled, err := d.isEventSchedulerEnabled()
if err != nil {
return err
}
if !eventSchedulerEnabled && info.HostExpiryEnabled {
return errors.New("MySQL Event Scheduler must be enabled to configure Host Expiry.")
}
if err := d.ManageHostExpiryEvent(info.HostExpiryEnabled, info.HostExpiryWindow); err != nil {
return err
}
// Note that we hard code the ID column to 1, insuring that, if no rows
// exist, a row will be created with INSERT, if a row does exist the key
// will be violate uniqueness constraint and an UPDATE will occur
insertStatement := `
INSERT INTO app_configs (
id,
org_name,
org_logo_url,
server_url,
smtp_configured,
smtp_sender_address,
smtp_server,
smtp_port,
smtp_authentication_type,
smtp_enable_ssl_tls,
smtp_authentication_method,
smtp_domain,
smtp_user_name,
smtp_password,
smtp_verify_ssl_certs,
smtp_enable_start_tls,
entity_id,
issuer_uri,
idp_image_url,
metadata,
metadata_url,
idp_name,
enable_sso,
enable_sso_idp_login,
fim_interval,
fim_file_accesses,
host_expiry_enabled,
host_expiry_window,
live_query_disabled,
additional_queries,
agent_options,
enable_analytics
)
VALUES( 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
ON DUPLICATE KEY UPDATE
org_name = VALUES(org_name),
org_logo_url = VALUES(org_logo_url),
server_url = VALUES(server_url),
smtp_configured = VALUES(smtp_configured),
smtp_sender_address = VALUES(smtp_sender_address),
smtp_server = VALUES(smtp_server),
smtp_port = VALUES(smtp_port),
smtp_authentication_type = VALUES(smtp_authentication_type),
smtp_enable_ssl_tls = VALUES(smtp_enable_ssl_tls),
smtp_authentication_method = VALUES(smtp_authentication_method),
smtp_domain = VALUES(smtp_domain),
smtp_user_name = VALUES(smtp_user_name),
smtp_password = VALUES(smtp_password),
smtp_verify_ssl_certs = VALUES(smtp_verify_ssl_certs),
smtp_enable_start_tls = VALUES(smtp_enable_start_tls),
entity_id = VALUES(entity_id),
issuer_uri = VALUES(issuer_uri),
idp_image_url = VALUES(idp_image_url),
metadata = VALUES(metadata),
metadata_url = VALUES(metadata_url),
idp_name = VALUES(idp_name),
enable_sso = VALUES(enable_sso),
enable_sso_idp_login = VALUES(enable_sso_idp_login),
fim_interval = VALUES(fim_interval),
fim_file_accesses = VALUES(fim_file_accesses),
host_expiry_enabled = VALUES(host_expiry_enabled),
host_expiry_window = VALUES(host_expiry_window),
live_query_disabled = VALUES(live_query_disabled),
additional_queries = VALUES(additional_queries),
agent_options = VALUES(agent_options),
enable_analytics = VALUES(enable_analytics)
`
_, err = d.db.Exec(insertStatement,
info.OrgName,
info.OrgLogoURL,
info.ServerURL,
info.SMTPConfigured,
info.SMTPSenderAddress,
info.SMTPServer,
info.SMTPPort,
info.SMTPAuthenticationType,
info.SMTPEnableTLS,
info.SMTPAuthenticationMethod,
info.SMTPDomain,
info.SMTPUserName,
info.SMTPPassword,
info.SMTPVerifySSLCerts,
info.SMTPEnableStartTLS,
info.EntityID,
info.IssuerURI,
info.IDPImageURL,
info.Metadata,
info.MetadataURL,
info.IDPName,
info.EnableSSO,
info.EnableSSOIdPLogin,
info.FIMInterval,
info.FIMFileAccesses,
info.HostExpiryEnabled,
info.HostExpiryWindow,
info.LiveQueryDisabled,
info.AdditionalQueries,
info.AgentOptions,
info.EnableAnalytics,
)
return err
}
func (d *Datastore) VerifyEnrollSecret(secret string) (*fleet.EnrollSecret, error) {
var s fleet.EnrollSecret
err := d.db.Get(&s, "SELECT team_id FROM enroll_secrets WHERE secret = ?", secret)
if err != nil {
return nil, errors.New("no matching secret found")
}
return &s, nil
}
func (d *Datastore) ApplyEnrollSecrets(teamID *uint, secrets []*fleet.EnrollSecret) error {
err := d.withRetryTxx(func(tx *sqlx.Tx) error {
if teamID != nil {
sql := `DELETE FROM enroll_secrets WHERE team_id = ?`
if _, err := tx.Exec(sql, teamID); err != nil {
return errors.Wrap(err, "clear before insert")
}
} else {
sql := `DELETE FROM enroll_secrets WHERE team_id IS NULL`
if _, err := tx.Exec(sql); err != nil {
return errors.Wrap(err, "clear before insert")
}
}
for _, secret := range secrets {
sql := `
INSERT INTO enroll_secrets (secret, team_id)
VALUES ( ?, ? )
`
if _, err := tx.Exec(sql, secret.Secret, teamID); err != nil {
return errors.Wrap(err, "upsert secret")
}
}
return nil
})
return err
}
func (d *Datastore) GetEnrollSecrets(teamID *uint) ([]*fleet.EnrollSecret, error) {
var args []interface{}
sql := "SELECT * FROM enroll_secrets WHERE "
// MySQL requires comparing NULL with IS. NULL = NULL evaluates to FALSE.
if teamID == nil {
sql += "team_id IS NULL"
} else {
sql += "team_id = ?"
args = append(args, teamID)
}
var secrets []*fleet.EnrollSecret
if err := d.db.Select(&secrets, sql, args...); err != nil {
return nil, errors.Wrap(err, "get secrets")
}
return secrets, nil
}