2022-01-25 14:34:00 +00:00
package service
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
2022-10-12 21:10:50 +00:00
"fmt"
2022-01-25 14:34:00 +00:00
"io"
"net"
2022-10-12 21:10:50 +00:00
"net/http"
2022-01-25 14:34:00 +00:00
"net/url"
2022-11-22 17:26:36 +00:00
"os"
2022-01-25 14:34:00 +00:00
2022-08-24 12:32:45 +00:00
"github.com/fleetdm/fleet/v4/server/authz"
2022-03-16 14:15:25 +00:00
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
2022-01-25 14:34:00 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
2022-09-19 17:53:44 +00:00
"github.com/go-kit/kit/log/level"
2022-01-25 14:34:00 +00:00
"github.com/kolide/kit/version"
)
////////////////////////////////////////////////////////////////////////////////
// Get AppConfig
////////////////////////////////////////////////////////////////////////////////
type appConfigResponse struct {
fleet . AppConfig
2022-08-24 21:40:09 +00:00
appConfigResponseFields
}
2022-01-25 14:34:00 +00:00
2022-08-24 21:40:09 +00:00
// appConfigResponseFields are grouped separately to aid with JSON unmarshaling
type appConfigResponseFields struct {
2022-01-25 14:34:00 +00:00
UpdateInterval * fleet . UpdateIntervalConfig ` json:"update_interval" `
Vulnerabilities * fleet . VulnerabilitiesConfig ` json:"vulnerabilities" `
// License is loaded from the service
License * fleet . LicenseInfo ` json:"license,omitempty" `
// Logging is loaded on the fly rather than from the database.
Logging * fleet . Logging ` json:"logging,omitempty" `
2022-07-12 21:21:15 +00:00
// SandboxEnabled is true if fleet serve was ran with server.sandbox_enabled=true
SandboxEnabled bool ` json:"sandbox_enabled,omitempty" `
2023-02-01 17:47:52 +00:00
// MDMFeatureFlagEnabled is true if fleet serve was ran with FLEET_DEV_MDM_ENABLED=1
2022-11-22 17:26:36 +00:00
//
// This is used only for UI development, for more details check
// https://github.com/fleetdm/fleet/issues/8751
//
// TODO: remove this flag once the MDM feature is ready for
// release.
2023-02-01 17:47:52 +00:00
MDMFeatureFlagEnabled bool ` json:"mdm_feature_flag_enabled,omitempty" `
Err error ` json:"error,omitempty" `
2022-01-25 14:34:00 +00:00
}
2022-08-24 21:40:09 +00:00
// UnmarshalJSON implements the json.Unmarshaler interface to make sure we serialize
// both AppConfig and appConfigResponseFields properly:
//
// - If this function is not defined, AppConfig.UnmarshalJSON gets promoted and
// will be called instead.
// - If we try to unmarshal everything in one go, AppConfig.UnmarshalJSON doesn't get
// called.
func ( r * appConfigResponse ) UnmarshalJSON ( data [ ] byte ) error {
if err := json . Unmarshal ( data , & r . AppConfig ) ; err != nil {
return err
}
if err := json . Unmarshal ( data , & r . appConfigResponseFields ) ; err != nil {
return err
}
return nil
}
2022-01-25 14:34:00 +00:00
func ( r appConfigResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func getAppConfigEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-25 14:34:00 +00:00
vc , ok := viewer . FromContext ( ctx )
if ! ok {
return nil , errors . New ( "could not fetch user" )
}
config , err := svc . AppConfig ( ctx )
if err != nil {
return nil , err
}
license , err := svc . License ( ctx )
if err != nil {
return nil , err
}
loggingConfig , err := svc . LoggingConfig ( ctx )
if err != nil {
return nil , err
}
updateIntervalConfig , err := svc . UpdateIntervalConfig ( ctx )
if err != nil {
return nil , err
}
vulnConfig , err := svc . VulnerabilitiesConfig ( ctx )
if err != nil {
return nil , err
}
var smtpSettings fleet . SMTPSettings
var ssoSettings fleet . SSOSettings
var hostExpirySettings fleet . HostExpirySettings
var agentOptions * json . RawMessage
// only admin can see smtp, sso, and host expiry settings
if vc . User . GlobalRole != nil && * vc . User . GlobalRole == fleet . RoleAdmin {
smtpSettings = config . SMTPSettings
ssoSettings = config . SSOSettings
hostExpirySettings = config . HostExpirySettings
agentOptions = config . AgentOptions
}
2022-06-10 15:39:02 +00:00
transparencyURL := fleet . DefaultTransparencyURL
// Fleet Premium license is required for custom transparency url
2022-08-10 18:15:35 +00:00
if license . IsPremium ( ) && config . FleetDesktop . TransparencyURL != "" {
2022-06-10 15:39:02 +00:00
transparencyURL = config . FleetDesktop . TransparencyURL
}
fleetDesktop := fleet . FleetDesktopSettings { TransparencyURL : transparencyURL }
2022-08-25 16:41:50 +00:00
features := config . Features
2022-01-25 14:34:00 +00:00
response := appConfigResponse {
AppConfig : fleet . AppConfig {
OrgInfo : config . OrgInfo ,
ServerSettings : config . ServerSettings ,
2022-08-25 16:41:50 +00:00
Features : features ,
2022-01-25 14:34:00 +00:00
VulnerabilitySettings : config . VulnerabilitySettings ,
SMTPSettings : smtpSettings ,
SSOSettings : ssoSettings ,
HostExpirySettings : hostExpirySettings ,
AgentOptions : agentOptions ,
2022-06-10 15:39:02 +00:00
FleetDesktop : fleetDesktop ,
2022-01-25 14:34:00 +00:00
WebhookSettings : config . WebhookSettings ,
2022-03-30 13:10:02 +00:00
Integrations : config . Integrations ,
2023-01-06 20:44:20 +00:00
MDM : config . MDM ,
2022-01-25 14:34:00 +00:00
} ,
2022-08-24 21:40:09 +00:00
appConfigResponseFields : appConfigResponseFields {
UpdateInterval : updateIntervalConfig ,
Vulnerabilities : vulnConfig ,
License : license ,
Logging : loggingConfig ,
SandboxEnabled : svc . SandboxEnabled ( ) ,
2023-02-01 17:47:52 +00:00
// Undocumented feature flag for MDM, used only for UI
// development, for more details check
// https://github.com/fleetdm/fleet/issues/8751
2022-11-22 17:26:36 +00:00
//
2023-02-01 17:47:52 +00:00
// TODO: remove this flag once the MDM feature is ready
// for release.
MDMFeatureFlagEnabled : os . Getenv ( "FLEET_DEV_MDM_ENABLED" ) == "1" ,
2022-08-24 21:40:09 +00:00
} ,
2022-01-25 14:34:00 +00:00
}
return response , nil
}
2022-07-12 21:21:15 +00:00
func ( svc * Service ) SandboxEnabled ( ) bool {
return svc . config . Server . SandboxEnabled
}
2022-01-25 14:34:00 +00:00
func ( svc * Service ) AppConfig ( ctx context . Context ) ( * fleet . AppConfig , error ) {
2022-03-16 14:15:25 +00:00
if ! svc . authz . IsAuthenticatedWith ( ctx , authz_ctx . AuthnDeviceToken ) {
if err := svc . authz . Authorize ( ctx , & fleet . AppConfig { } , fleet . ActionRead ) ; err != nil {
return nil , err
}
2022-01-25 14:34:00 +00:00
}
2022-04-18 20:55:51 +00:00
ac , err := svc . ds . AppConfig ( ctx )
if err != nil {
return nil , err
}
if ac . SMTPSettings . SMTPPassword != "" {
2022-06-06 14:41:51 +00:00
ac . SMTPSettings . SMTPPassword = fleet . MaskedPassword
2022-04-18 20:55:51 +00:00
}
for _ , jiraIntegration := range ac . Integrations . Jira {
2022-06-06 14:41:51 +00:00
jiraIntegration . APIToken = fleet . MaskedPassword
2022-04-18 20:55:51 +00:00
}
2022-05-02 20:58:34 +00:00
for _ , zdIntegration := range ac . Integrations . Zendesk {
2022-06-06 14:41:51 +00:00
zdIntegration . APIToken = fleet . MaskedPassword
2022-05-02 20:58:34 +00:00
}
2022-04-18 20:55:51 +00:00
return ac , nil
2022-01-25 14:34:00 +00:00
}
////////////////////////////////////////////////////////////////////////////////
// Modify AppConfig
////////////////////////////////////////////////////////////////////////////////
type modifyAppConfigRequest struct {
2022-09-19 17:53:44 +00:00
Force bool ` json:"-" query:"force,optional" ` // if true, bypass strict incoming json validation
DryRun bool ` json:"-" query:"dry_run,optional" ` // if true, apply validation but do not save changes
2022-01-25 14:34:00 +00:00
json . RawMessage
}
2022-12-27 14:26:59 +00:00
func modifyAppConfigEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-25 14:34:00 +00:00
req := request . ( * modifyAppConfigRequest )
2022-09-19 17:53:44 +00:00
config , err := svc . ModifyAppConfig ( ctx , req . RawMessage , fleet . ApplySpecOptions {
Force : req . Force ,
DryRun : req . DryRun ,
} )
2022-01-25 14:34:00 +00:00
if err != nil {
2022-08-24 21:40:09 +00:00
return appConfigResponse { appConfigResponseFields : appConfigResponseFields { Err : err } } , nil
2022-01-25 14:34:00 +00:00
}
2022-10-17 19:03:49 +00:00
2022-01-25 14:34:00 +00:00
license , err := svc . License ( ctx )
if err != nil {
return nil , err
}
loggingConfig , err := svc . LoggingConfig ( ctx )
if err != nil {
return nil , err
}
response := appConfigResponse {
AppConfig : * config ,
2022-08-24 21:40:09 +00:00
appConfigResponseFields : appConfigResponseFields {
License : license ,
Logging : loggingConfig ,
} ,
2022-01-25 14:34:00 +00:00
}
if response . SMTPSettings . SMTPPassword != "" {
2022-06-06 14:41:51 +00:00
response . SMTPSettings . SMTPPassword = fleet . MaskedPassword
2022-01-25 14:34:00 +00:00
}
2022-06-10 15:39:02 +00:00
if license . Tier != "premium" || response . FleetDesktop . TransparencyURL == "" {
response . FleetDesktop . TransparencyURL = fleet . DefaultTransparencyURL
}
2022-01-25 14:34:00 +00:00
return response , nil
}
2022-09-19 17:53:44 +00:00
func ( svc * Service ) ModifyAppConfig ( ctx context . Context , p [ ] byte , applyOpts fleet . ApplySpecOptions ) ( * fleet . AppConfig , error ) {
2022-01-25 14:34:00 +00:00
if err := svc . authz . Authorize ( ctx , & fleet . AppConfig { } , fleet . ActionWrite ) ; err != nil {
return nil , err
}
2022-05-02 20:58:34 +00:00
// we need the config from the datastore because API tokens are obfuscated at the service layer
// we will retrieve the obfuscated config before we return
appConfig , err := svc . ds . AppConfig ( ctx )
2022-01-25 14:34:00 +00:00
if err != nil {
return nil , err
}
2023-01-03 22:14:18 +00:00
oldAppConfig := appConfig . Copy ( )
2022-01-25 14:34:00 +00:00
2022-06-10 15:39:02 +00:00
license , err := svc . License ( ctx )
if err != nil {
return nil , err
}
2022-02-28 21:28:51 +00:00
oldSmtpSettings := appConfig . SMTPSettings
2022-08-24 12:32:45 +00:00
oldAgentOptions := ""
if appConfig . AgentOptions != nil {
oldAgentOptions = string ( * appConfig . AgentOptions )
}
2022-05-02 20:58:34 +00:00
2022-06-06 14:41:51 +00:00
storedJiraByProjectKey , err := fleet . IndexJiraIntegrations ( appConfig . Integrations . Jira )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "modify AppConfig" )
2022-05-02 20:58:34 +00:00
}
2022-06-06 14:41:51 +00:00
storedZendeskByGroupID , err := fleet . IndexZendeskIntegrations ( appConfig . Integrations . Zendesk )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "modify AppConfig" )
2022-04-06 11:55:25 +00:00
}
2022-01-25 14:34:00 +00:00
invalid := & fleet . InvalidArgumentError { }
var newAppConfig fleet . AppConfig
if err := json . Unmarshal ( p , & newAppConfig ) ; err != nil {
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , ctxerr . Wrap ( ctx , & fleet . BadRequestError {
Message : "failed to decode app config" ,
InternalErr : err ,
} )
2022-01-25 14:34:00 +00:00
}
2022-03-30 13:10:02 +00:00
2023-03-06 15:44:40 +00:00
// default transparency URL is https://fleetdm.com/transparency so you are allowed to apply as long as it's not changing
if newAppConfig . FleetDesktop . TransparencyURL != "" && newAppConfig . FleetDesktop . TransparencyURL != fleet . DefaultTransparencyURL {
2022-06-13 21:03:51 +00:00
if license . Tier != "premium" {
invalid . Append ( "transparency_url" , ErrMissingLicense . Error ( ) )
return nil , ctxerr . Wrap ( ctx , invalid )
}
if _ , err := url . Parse ( newAppConfig . FleetDesktop . TransparencyURL ) ; err != nil {
invalid . Append ( "transparency_url" , err . Error ( ) )
return nil , ctxerr . Wrap ( ctx , invalid )
}
}
2022-08-10 18:15:35 +00:00
validateSSOSettings ( newAppConfig , appConfig , invalid , license )
2022-01-25 14:34:00 +00:00
if invalid . HasErrors ( ) {
return nil , ctxerr . Wrap ( ctx , invalid )
}
// We apply the config that is incoming to the old one
2022-08-24 21:40:09 +00:00
appConfig . EnableStrictDecoding ( )
if err := json . Unmarshal ( p , & appConfig ) ; err != nil {
2022-10-12 21:10:50 +00:00
err = fleet . NewUserMessageError ( err , http . StatusBadRequest )
return nil , ctxerr . Wrap ( ctx , err )
2022-09-19 17:53:44 +00:00
}
var legacyUsedWarning error
2022-10-12 21:10:50 +00:00
if legacyKeys := appConfig . DidUnmarshalLegacySettings ( ) ; len ( legacyKeys ) > 0 {
2022-09-19 17:53:44 +00:00
// this "warning" is returned only in dry-run mode, and if no other errors
// were encountered.
legacyUsedWarning = & fleet . BadRequestError {
2022-10-12 21:10:50 +00:00
Message : fmt . Sprintf ( "warning: deprecated settings were used in the configuration: %v; consider updating to the new settings: https://fleetdm.com/docs/using-fleet/configuration-files#settings" , legacyKeys ) ,
2022-09-19 17:53:44 +00:00
}
}
// required fields must be set, ensure they haven't been removed by applying
// the new config
if appConfig . OrgInfo . OrgName == "" {
invalid . Append ( "org_name" , "organization name must be present" )
}
if appConfig . ServerSettings . ServerURL == "" {
invalid . Append ( "server_url" , "Fleet server URL must be present" )
}
if newAppConfig . AgentOptions != nil {
// if there were Agent Options in the new app config, then it replaced the
// agent options in the resulting app config, so validate those.
if err := fleet . ValidateJSONAgentOptions ( * appConfig . AgentOptions ) ; err != nil {
2022-10-12 21:10:50 +00:00
err = fleet . NewUserMessageError ( err , http . StatusBadRequest )
2022-09-19 17:53:44 +00:00
if applyOpts . Force && ! applyOpts . DryRun {
level . Info ( svc . logger ) . Log ( "err" , err , "msg" , "force-apply appConfig agent options with validation errors" )
}
if ! applyOpts . Force {
2022-10-12 21:10:50 +00:00
return nil , ctxerr . Wrap ( ctx , err , "validate agent options" )
2022-09-19 17:53:44 +00:00
}
}
2022-01-25 14:34:00 +00:00
}
2022-06-06 14:41:51 +00:00
fleet . ValidateEnabledVulnerabilitiesIntegrations ( appConfig . WebhookSettings . VulnerabilitiesWebhook , appConfig . Integrations , invalid )
fleet . ValidateEnabledFailingPoliciesIntegrations ( appConfig . WebhookSettings . FailingPoliciesWebhook , appConfig . Integrations , invalid )
2022-09-19 17:53:44 +00:00
fleet . ValidateEnabledHostStatusIntegrations ( appConfig . WebhookSettings . HostStatusWebhook , invalid )
2023-01-03 22:14:18 +00:00
svc . validateMDM ( ctx , license , & oldAppConfig . MDM , & appConfig . MDM , invalid )
2022-03-30 13:10:02 +00:00
if invalid . HasErrors ( ) {
return nil , ctxerr . Wrap ( ctx , invalid )
}
2023-02-01 17:47:52 +00:00
// ignore AppleBMTermsExpired and Enabled if provided in the modify
// payload we don't return an error in this case because it would
// prevent using the output of fleetctl get config as input to fleetctl
// apply or this endpoint.
2023-02-28 20:34:46 +00:00
appConfig . MDM . AppleBMTermsExpired = oldAppConfig . MDM . AppleBMTermsExpired
appConfig . MDM . EnabledAndConfigured = oldAppConfig . MDM . EnabledAndConfigured
2023-01-06 20:44:20 +00:00
2022-09-19 17:53:44 +00:00
// do not send a test email in dry-run mode, so this is a good place to stop
// (we also delete the removed integrations after that, which we don't want
// to do in dry-run mode).
if applyOpts . DryRun {
if legacyUsedWarning != nil {
return nil , legacyUsedWarning
}
// must reload to get the unchanged app config
return svc . AppConfig ( ctx )
}
2022-02-28 21:28:51 +00:00
// ignore the values for SMTPEnabled and SMTPConfigured
oldSmtpSettings . SMTPEnabled = appConfig . SMTPSettings . SMTPEnabled
oldSmtpSettings . SMTPConfigured = appConfig . SMTPSettings . SMTPConfigured
// if we enable SMTP and the settings have changed, then we send a test email
if appConfig . SMTPSettings . SMTPEnabled {
if oldSmtpSettings != appConfig . SMTPSettings || ! appConfig . SMTPSettings . SMTPConfigured {
if err = svc . sendTestEmail ( ctx , appConfig ) ; err != nil {
2022-04-06 11:55:25 +00:00
return nil , ctxerr . Wrap ( ctx , err )
2022-02-28 21:28:51 +00:00
}
2022-01-25 14:34:00 +00:00
}
appConfig . SMTPSettings . SMTPConfigured = true
2022-05-31 10:19:57 +00:00
} else {
2022-01-25 14:34:00 +00:00
appConfig . SMTPSettings . SMTPConfigured = false
}
2022-06-13 14:04:47 +00:00
delJira , err := fleet . ValidateJiraIntegrations ( ctx , storedJiraByProjectKey , newAppConfig . Integrations . Jira )
if err != nil {
2022-06-06 14:41:51 +00:00
if errors . As ( err , & fleet . IntegrationTestError { } ) {
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , ctxerr . Wrap ( ctx , & fleet . BadRequestError {
Message : err . Error ( ) ,
} )
2022-04-06 11:55:25 +00:00
}
2022-06-06 14:41:51 +00:00
return nil , ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "Jira integration" , err . Error ( ) ) )
2022-05-02 20:58:34 +00:00
}
2022-06-06 14:41:51 +00:00
appConfig . Integrations . Jira = newAppConfig . Integrations . Jira
2022-05-02 20:58:34 +00:00
2022-06-13 14:04:47 +00:00
delZendesk , err := fleet . ValidateZendeskIntegrations ( ctx , storedZendeskByGroupID , newAppConfig . Integrations . Zendesk )
if err != nil {
2022-06-06 14:41:51 +00:00
if errors . As ( err , & fleet . IntegrationTestError { } ) {
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , ctxerr . Wrap ( ctx , & fleet . BadRequestError {
Message : err . Error ( ) ,
} )
2022-05-02 20:58:34 +00:00
}
2022-06-06 14:41:51 +00:00
return nil , ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "Zendesk integration" , err . Error ( ) ) )
2022-04-06 11:55:25 +00:00
}
2022-06-06 14:41:51 +00:00
appConfig . Integrations . Zendesk = newAppConfig . Integrations . Zendesk
2022-04-06 11:55:25 +00:00
2022-06-13 14:04:47 +00:00
// if any integration was deleted, remove it from any team that uses it
if len ( delJira ) + len ( delZendesk ) > 0 {
if err := svc . ds . DeleteIntegrationsFromTeams ( ctx , fleet . Integrations { Jira : delJira , Zendesk : delZendesk } ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "delete integrations from teams" )
}
}
2023-01-03 22:14:18 +00:00
if license . Tier != "premium" {
// reset transparency url to empty for downgraded licenses
2022-06-13 21:03:51 +00:00
appConfig . FleetDesktop . TransparencyURL = ""
2022-06-10 15:39:02 +00:00
}
2022-01-25 14:34:00 +00:00
if err := svc . ds . SaveAppConfig ( ctx , appConfig ) ; err != nil {
return nil , err
}
2022-05-02 20:58:34 +00:00
// retrieve new app config with obfuscated secrets
obfuscatedConfig , err := svc . AppConfig ( ctx )
if err != nil {
return nil , err
}
2022-08-24 12:32:45 +00:00
// if the agent options changed, create the corresponding activity
newAgentOptions := ""
if obfuscatedConfig . AgentOptions != nil {
newAgentOptions = string ( * obfuscatedConfig . AgentOptions )
}
if oldAgentOptions != newAgentOptions {
if err := svc . ds . NewActivity (
ctx ,
authz . UserFromContext ( ctx ) ,
2022-12-23 16:05:16 +00:00
fleet . ActivityTypeEditedAgentOptions {
Global : true ,
} ,
2022-08-24 12:32:45 +00:00
) ; err != nil {
2023-01-31 22:36:18 +00:00
return nil , ctxerr . Wrap ( ctx , err , "create activity for app config agent options modification" )
}
}
// if the macOS minimum version requirement changed, create the corresponding
// activity
if oldAppConfig . MDM . MacOSUpdates != appConfig . MDM . MacOSUpdates {
if err := svc . ds . NewActivity (
ctx ,
authz . UserFromContext ( ctx ) ,
fleet . ActivityTypeEditedMacOSMinVersion {
MinimumVersion : appConfig . MDM . MacOSUpdates . MinimumVersion ,
Deadline : appConfig . MDM . MacOSUpdates . Deadline ,
} ,
) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "create activity for app config macos min version modification" )
2022-08-24 12:32:45 +00:00
}
}
2023-03-08 13:31:53 +00:00
if oldAppConfig . MDM . MacOSSettings . EnableDiskEncryption != appConfig . MDM . MacOSSettings . EnableDiskEncryption {
var act fleet . ActivityDetails
if appConfig . MDM . MacOSSettings . EnableDiskEncryption {
act = fleet . ActivityTypeEnabledMacosDiskEncryption { }
if err := svc . EnterpriseOverrides . MDMAppleEnableFileVaultAndEscrow ( ctx , nil ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "enable no-team filevault and escrow" )
}
} else {
act = fleet . ActivityTypeDisabledMacosDiskEncryption { }
if err := svc . EnterpriseOverrides . MDMAppleDisableFileVaultAndEscrow ( ctx , nil ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "disable no-team filevault and escrow" )
}
}
if err := svc . ds . NewActivity ( ctx , authz . UserFromContext ( ctx ) , act ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "create activity for app config macos disk encryption" )
}
}
2022-05-02 20:58:34 +00:00
return obfuscatedConfig , nil
2022-01-25 14:34:00 +00:00
}
2023-01-03 22:14:18 +00:00
func ( svc * Service ) validateMDM (
ctx context . Context ,
license * fleet . LicenseInfo ,
oldMdm * fleet . MDM ,
mdm * fleet . MDM ,
invalid * fleet . InvalidArgumentError ,
) {
2023-02-28 20:34:46 +00:00
if mdm . MacOSSettings . EnableDiskEncryption && ! license . IsPremium ( ) {
invalid . Append ( "macos_settings.enable_disk_encryption" , ErrMissingLicense . Error ( ) )
}
if ! svc . config . MDMApple . Enable {
// TODO(mna): eventually we should detect the minimum config required for
// this to be allowed, probably just SCEP/APNs?
if len ( mdm . MacOSSettings . CustomSettings ) != 0 {
invalid . Append ( "macos_settings.custom_settings" ,
` Couldn't update macos_settings because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features. ` )
}
if mdm . MacOSSettings . EnableDiskEncryption {
invalid . Append ( "macos_settings.enable_disk_encryption" ,
` Couldn't update macos_settings because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features. ` )
}
}
2023-01-03 22:14:18 +00:00
if name := mdm . AppleBMDefaultTeam ; name != "" && name != oldMdm . AppleBMDefaultTeam {
if ! license . IsPremium ( ) {
invalid . Append ( "mdm.apple_bm_default_team" , ErrMissingLicense . Error ( ) )
return
}
if _ , err := svc . ds . TeamByName ( ctx , name ) ; err != nil {
invalid . Append ( "apple_bm_default_team" , "team name not found" )
}
}
2023-01-24 16:20:02 +00:00
// MacOSUpdates
updatingVersion := mdm . MacOSUpdates . MinimumVersion != "" &&
mdm . MacOSUpdates . MinimumVersion != oldMdm . MacOSUpdates . MinimumVersion
updatingDeadline := mdm . MacOSUpdates . Deadline != "" &&
mdm . MacOSUpdates . Deadline != oldMdm . MacOSUpdates . Deadline
if updatingVersion || updatingDeadline {
if ! license . IsPremium ( ) {
invalid . Append ( "macos_updates.minimum_version" , ErrMissingLicense . Error ( ) )
return
}
if err := mdm . MacOSUpdates . Validate ( ) ; err != nil {
invalid . Append ( "macos_updates" , err . Error ( ) )
}
}
2023-01-03 22:14:18 +00:00
}
2022-08-10 18:15:35 +00:00
func validateSSOSettings ( p fleet . AppConfig , existing * fleet . AppConfig , invalid * fleet . InvalidArgumentError , license * fleet . LicenseInfo ) {
2022-01-25 14:34:00 +00:00
if p . SSOSettings . EnableSSO {
if p . SSOSettings . Metadata == "" && p . SSOSettings . MetadataURL == "" {
if existing . SSOSettings . Metadata == "" && existing . SSOSettings . MetadataURL == "" {
invalid . Append ( "metadata" , "either metadata or metadata_url must be defined" )
}
}
if p . SSOSettings . Metadata != "" && p . SSOSettings . MetadataURL != "" {
invalid . Append ( "metadata" , "both metadata and metadata_url are defined, only one is allowed" )
}
if p . SSOSettings . EntityID == "" {
if existing . SSOSettings . EntityID == "" {
invalid . Append ( "entity_id" , "required" )
}
} else {
if len ( p . SSOSettings . EntityID ) < 5 {
invalid . Append ( "entity_id" , "must be 5 or more characters" )
}
}
if p . SSOSettings . IDPName == "" {
if existing . SSOSettings . IDPName == "" {
invalid . Append ( "idp_name" , "required" )
}
}
2023-03-14 20:17:08 +00:00
if ! license . IsPremium ( ) {
if p . SSOSettings . EnableJITProvisioning {
invalid . Append ( "enable_jit_provisioning" , ErrMissingLicense . Error ( ) )
}
if p . SSOSettings . EnableJITRoleSync {
invalid . Append ( "enable_jit_role_sync" , ErrMissingLicense . Error ( ) )
}
2022-08-10 18:15:35 +00:00
}
2022-01-25 14:34:00 +00:00
}
}
////////////////////////////////////////////////////////////////////////////////
// Apply enroll secret spec
////////////////////////////////////////////////////////////////////////////////
type applyEnrollSecretSpecRequest struct {
Spec * fleet . EnrollSecretSpec ` json:"spec" `
}
type applyEnrollSecretSpecResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r applyEnrollSecretSpecResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func applyEnrollSecretSpecEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-25 14:34:00 +00:00
req := request . ( * applyEnrollSecretSpecRequest )
err := svc . ApplyEnrollSecretSpec ( ctx , req . Spec )
if err != nil {
return applyEnrollSecretSpecResponse { Err : err } , nil
}
return applyEnrollSecretSpecResponse { } , nil
}
func ( svc * Service ) ApplyEnrollSecretSpec ( ctx context . Context , spec * fleet . EnrollSecretSpec ) error {
if err := svc . authz . Authorize ( ctx , & fleet . EnrollSecret { } , fleet . ActionWrite ) ; err != nil {
return err
}
2022-10-05 12:35:36 +00:00
if len ( spec . Secrets ) > fleet . MaxEnrollSecretsCount {
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "secrets" , "too many secrets" ) )
}
2022-01-25 14:34:00 +00:00
for _ , s := range spec . Secrets {
if s . Secret == "" {
return ctxerr . New ( ctx , "enroll secret must not be empty" )
}
}
2022-07-12 22:12:10 +00:00
if svc . config . Packaging . GlobalEnrollSecret != "" {
return ctxerr . New ( ctx , "enroll secret cannot be changed when fleet_packaging.global_enroll_secret is set" )
}
2022-01-25 14:34:00 +00:00
return svc . ds . ApplyEnrollSecrets ( ctx , nil , spec . Secrets )
}
////////////////////////////////////////////////////////////////////////////////
// Get enroll secret spec
////////////////////////////////////////////////////////////////////////////////
type getEnrollSecretSpecResponse struct {
Spec * fleet . EnrollSecretSpec ` json:"spec" `
Err error ` json:"error,omitempty" `
}
func ( r getEnrollSecretSpecResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func getEnrollSecretSpecEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-25 14:34:00 +00:00
specs , err := svc . GetEnrollSecretSpec ( ctx )
if err != nil {
return getEnrollSecretSpecResponse { Err : err } , nil
}
return getEnrollSecretSpecResponse { Spec : specs } , nil
}
func ( svc * Service ) GetEnrollSecretSpec ( ctx context . Context ) ( * fleet . EnrollSecretSpec , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . EnrollSecret { } , fleet . ActionRead ) ; err != nil {
return nil , err
}
secrets , err := svc . ds . GetEnrollSecrets ( ctx , nil )
if err != nil {
return nil , err
}
return & fleet . EnrollSecretSpec { Secrets : secrets } , nil
}
////////////////////////////////////////////////////////////////////////////////
// Version
////////////////////////////////////////////////////////////////////////////////
type versionResponse struct {
* version . Info
Err error ` json:"error,omitempty" `
}
func ( r versionResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func versionEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-25 14:34:00 +00:00
info , err := svc . Version ( ctx )
if err != nil {
return versionResponse { Err : err } , nil
}
return versionResponse { Info : info } , nil
}
func ( svc * Service ) Version ( ctx context . Context ) ( * version . Info , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . AppConfig { } , fleet . ActionRead ) ; err != nil {
return nil , err
}
info := version . Version ( )
return & info , nil
}
////////////////////////////////////////////////////////////////////////////////
// Get Certificate Chain
////////////////////////////////////////////////////////////////////////////////
type getCertificateResponse struct {
CertificateChain [ ] byte ` json:"certificate_chain" `
Err error ` json:"error,omitempty" `
}
func ( r getCertificateResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func getCertificateEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-01-25 14:34:00 +00:00
chain , err := svc . CertificateChain ( ctx )
if err != nil {
return getCertificateResponse { Err : err } , nil
}
return getCertificateResponse { CertificateChain : chain } , nil
}
// Certificate returns the PEM encoded certificate chain for osqueryd TLS termination.
func ( svc * Service ) CertificateChain ( ctx context . Context ) ( [ ] byte , error ) {
config , err := svc . AppConfig ( ctx )
if err != nil {
return nil , err
}
u , err := url . Parse ( config . ServerSettings . ServerURL )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "parsing serverURL" )
}
conn , err := connectTLS ( ctx , u )
if err != nil {
return nil , err
}
return chain ( ctx , conn . ConnectionState ( ) , u . Hostname ( ) )
}
func connectTLS ( ctx context . Context , serverURL * url . URL ) ( * tls . Conn , error ) {
var hostport string
if serverURL . Port ( ) == "" {
hostport = net . JoinHostPort ( serverURL . Host , "443" )
} else {
hostport = serverURL . Host
}
// attempt dialing twice, first with a secure conn, and then
// if that fails, use insecure
dial := func ( insecure bool ) ( * tls . Conn , error ) {
conn , err := tls . Dial ( "tcp" , hostport , & tls . Config {
2022-02-14 03:35:59 +00:00
InsecureSkipVerify : insecure ,
} )
2022-01-25 14:34:00 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "dial tls" )
}
defer conn . Close ( )
return conn , nil
}
var (
conn * tls . Conn
err error
)
conn , err = dial ( false )
if err == nil {
return conn , nil
}
conn , err = dial ( true )
return conn , err
}
// chain builds a PEM encoded certificate chain using the PeerCertificates
// in tls.ConnectionState. chain uses the hostname to omit the Leaf certificate
// from the chain.
func chain ( ctx context . Context , cs tls . ConnectionState , hostname string ) ( [ ] byte , error ) {
buf := bytes . NewBuffer ( [ ] byte ( "" ) )
verifyEncode := func ( chain [ ] * x509 . Certificate ) error {
for _ , cert := range chain {
if len ( chain ) > 1 {
// drop the leaf certificate from the chain. osqueryd does not
// need it to establish a secure connection
if err := cert . VerifyHostname ( hostname ) ; err == nil {
continue
}
}
if err := encodePEMCertificate ( buf , cert ) ; err != nil {
return err
}
}
return nil
}
// use verified chains if available(which adds the root CA), otherwise
// use the certificate chain offered by the server (if terminated with
// self-signed certs)
if len ( cs . VerifiedChains ) != 0 {
for _ , chain := range cs . VerifiedChains {
if err := verifyEncode ( chain ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "encode verified chains pem" )
}
}
} else {
if err := verifyEncode ( cs . PeerCertificates ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "encode peer certificates pem" )
}
}
return buf . Bytes ( ) , nil
}
func encodePEMCertificate ( buf io . Writer , cert * x509 . Certificate ) error {
block := & pem . Block {
Type : "CERTIFICATE" ,
Bytes : cert . Raw ,
}
return pem . Encode ( buf , block )
}
2022-08-30 11:13:09 +00:00
func ( svc * Service ) HostFeatures ( ctx context . Context , host * fleet . Host ) ( * fleet . Features , error ) {
if svc . EnterpriseOverrides != nil {
return svc . EnterpriseOverrides . HostFeatures ( ctx , host )
}
appConfig , err := svc . ds . AppConfig ( ctx )
if err != nil {
return nil , err
}
return & appConfig . Features , nil
}