From c1aa8355cb09903a21ffa0b6c6ca1933436f8a2f Mon Sep 17 00:00:00 2001 From: Zachary Wasserman Date: Fri, 29 May 2020 09:12:39 -0700 Subject: [PATCH] Add support for multiple enroll secrets (#2238) - Support multiple enroll secrets - Record name of enroll secret used when host enrolls - Update fleetctl and UI to support these features --- cmd/fleetctl/apply.go | 30 +- cmd/fleetctl/get.go | 20 +- docs/cli/file-format.md | 23 +- docs/infrastructure/adding-hosts-to-fleet.md | 21 +- examples/config-many-files/enroll-secret.yml | 14 + examples/config-single-file.yml | 14 + frontend/components/App/App.jsx | 6 +- .../EnrollSecretTable/EnrollSecretTable.jsx | 122 ++++++ .../EnrollSecretTable.tests.jsx | 76 ++++ .../config/EnrollSecretTable/_styles.scss | 9 + .../config/EnrollSecretTable/index.js | 1 + .../components/forms/FormField/FormField.jsx | 2 +- .../admin/AppConfigForm/AppConfigForm.jsx | 31 +- .../AppConfigForm/AppConfigForm.tests.jsx | 12 + .../hosts/AddHostModal/AddHostModal.jsx | 83 +--- .../hosts/AddHostModal/AddHostModal.tests.jsx | 32 -- frontend/interfaces/enroll_secret.js | 10 + frontend/kolide/entities/config.js | 5 + .../admin/AppSettingsPage/AppSettingsPage.jsx | 9 +- .../AppSettingsPage/AppSettingsPage.tests.jsx | 18 +- .../hosts/ManageHostsPage/ManageHostsPage.jsx | 12 +- .../ManageHostsPage/ManageHostsPage.tests.jsx | 1 + frontend/redux/nodes/app/actions.js | 29 ++ frontend/redux/nodes/app/actions.tests.js | 44 ++- frontend/redux/nodes/app/reducer.js | 22 ++ frontend/redux/nodes/app/reducer.tests.js | 52 ++- frontend/utilities/copy_text.js | 6 + server/datastore/datastore_app_test.go | 76 ++++ server/datastore/datastore_hosts_test.go | 31 +- server/datastore/datastore_labels_test.go | 2 +- server/datastore/datastore_targets_test.go | 2 +- server/datastore/datastore_test.go | 2 + server/datastore/inmem/hosts.go | 7 +- server/datastore/mysql/app_configs.go | 48 ++- server/datastore/mysql/hosts.go | 22 +- ...20200512120000_CreateTableEnrollSecrets.go | 56 +++ server/kolide/app.go | 43 ++- server/kolide/hosts.go | 5 +- server/mock/datastore_appconfig.go | 30 ++ server/mock/datastore_hosts.go | 6 +- server/service/client_appconfig.go | 58 +++ server/service/endpoint_appconfig.go | 48 ++- server/service/endpoint_middleware_test.go | 33 +- server/service/endpoint_setup.go | 4 - server/service/handler.go | 10 + server/service/service_appconfig.go | 40 +- server/service/service_appconfig_test.go | 35 +- server/service/service_osquery.go | 19 +- server/service/service_osquery_test.go | 357 +++++++++--------- server/service/transport_appconfig.go | 9 + 50 files changed, 1218 insertions(+), 429 deletions(-) create mode 100644 examples/config-many-files/enroll-secret.yml create mode 100644 frontend/components/config/EnrollSecretTable/EnrollSecretTable.jsx create mode 100644 frontend/components/config/EnrollSecretTable/EnrollSecretTable.tests.jsx create mode 100644 frontend/components/config/EnrollSecretTable/_styles.scss create mode 100644 frontend/components/config/EnrollSecretTable/index.js delete mode 100644 frontend/components/hosts/AddHostModal/AddHostModal.tests.jsx create mode 100644 frontend/interfaces/enroll_secret.js create mode 100644 server/datastore/mysql/migrations/tables/20200512120000_CreateTableEnrollSecrets.go diff --git a/cmd/fleetctl/apply.go b/cmd/fleetctl/apply.go index 0250f906f..9d089f19d 100644 --- a/cmd/fleetctl/apply.go +++ b/cmd/fleetctl/apply.go @@ -19,11 +19,12 @@ type specMetadata struct { } type specGroup struct { - Queries []*kolide.QuerySpec - Packs []*kolide.PackSpec - Labels []*kolide.LabelSpec - Options *kolide.OptionsSpec - AppConfig *kolide.AppConfigPayload + Queries []*kolide.QuerySpec + Packs []*kolide.PackSpec + Labels []*kolide.LabelSpec + Options *kolide.OptionsSpec + AppConfig *kolide.AppConfigPayload + EnrollSecret *kolide.EnrollSecretSpec } func specGroupFromBytes(b []byte) (*specGroup, error) { @@ -91,6 +92,17 @@ func specGroupFromBytes(b []byte) (*specGroup, error) { } specs.AppConfig = appConfigSpec + case "enroll_secret": + if specs.AppConfig != nil { + return nil, errors.New("enroll_secret defined twice in the same file") + } + + var enrollSecretSpec *kolide.EnrollSecretSpec + if err := yaml.Unmarshal(s.Spec, &enrollSecretSpec); err != nil { + return nil, errors.Wrap(err, "unmarshaling enroll secret spec") + } + specs.EnrollSecret = enrollSecretSpec + default: return nil, errors.Errorf("unknown kind %q", s.Kind) } @@ -181,6 +193,14 @@ func applyCommand() cli.Command { } + if specs.EnrollSecret != nil { + if err := fleet.ApplyEnrollSecretSpec(specs.EnrollSecret); err != nil { + return errors.Wrap(err, "applying enroll secrets") + } + fmt.Printf("[+] applied enroll secrets\n") + + } + return nil }, } diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 5b46104b3..bc1fa2630 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -392,8 +392,9 @@ func getOptionsCommand() cli.Command { func getEnrollSecretCommand() cli.Command { return cli.Command{ - Name: "enroll-secret", - Usage: "Retrieve the osquery enroll secret", + Name: "enroll_secret", + Aliases: []string{"enroll_secrets", "enroll-secret", "enroll-secrets"}, + Usage: "Retrieve the osquery enroll secrets", Flags: []cli.Flag{ configFlag(), contextFlag(), @@ -404,16 +405,23 @@ func getEnrollSecretCommand() cli.Command { return err } - settings, err := fleet.GetServerSettings() + secrets, err := fleet.GetEnrollSecretSpec() if err != nil { return err } - if settings == nil { - return errors.New("error: server setting were nil") + + spec := specGeneric{ + Kind: "enroll_secret", + Version: kolide.ApiVersion, + Spec: secrets, } - fmt.Println(*settings.EnrollSecret) + b, err := yaml.Marshal(spec) + if err != nil { + return err + } + fmt.Print(string(b)) return nil }, } diff --git a/docs/cli/file-format.md b/docs/cli/file-format.md index 90916435e..db84d3b23 100644 --- a/docs/cli/file-format.md +++ b/docs/cli/file-format.md @@ -255,7 +255,6 @@ spec: org_name: Example Org server_settings: kolide_server_url: https://fleet.example.org:8080 - osquery_enroll_secret: supersekretsecret smtp_settings: authentication_method: authmethod_plain authentication_type: authtype_username_password @@ -291,3 +290,25 @@ The following options are available when configuring SMTP authentication: - `authmethod_cram_md5` - `authmethod_login` - `authmethod_plain` + +## Enroll Secrets + +The following file shows how to configure enroll secrets. Note that secrets can be changed or made inactive, but not deleted. Hosts may not enroll with inactive secrets. + +The name of the enroll secret used to authenticate is stored with the host and is included with API results. + +```yaml +apiVersion: v1 +kind: enroll_secret +spec: + secrets: + - active: true + name: default + secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff + - active: true + name: new_one + secret: reallyworks + - active: false + name: inactive_secret + secret: thissecretwontwork! +``` diff --git a/docs/infrastructure/adding-hosts-to-fleet.md b/docs/infrastructure/adding-hosts-to-fleet.md index 432463fb8..191ad264d 100644 --- a/docs/infrastructure/adding-hosts-to-fleet.md +++ b/docs/infrastructure/adding-hosts-to-fleet.md @@ -20,7 +20,8 @@ To directly execute the launcher binary without having to mess with packages, in - `--hostname`: the hostname of the gRPC server for your environment - `--root_directory`: the location of the local database, pidfiles, etc. -- `--enroll_secret`: the enroll secret you generated above for your environment +- `--enroll_secret`: the enroll secret to authenticate hosts with Fleet + (retrieve from Fleet UI or `fleetctl get enroll_secret`) ``` ./build/launcher \ @@ -45,7 +46,8 @@ $ ./build/package-builder make \ As you can see, to generate a Launcher package, you need only call `package-builder make` with two command-line arguments: - `--hostname`: the hostname of the gRPC server for your environment -- `--enroll_secret`: the enroll secret you generated above for your environment +- `--enroll_secret`: the enroll secret to authenticate hosts with Fleet + (retrieve from Fleet UI or `fleetctl get enroll_secret`) You can also add the `--mac_package_signing_key` flag to define the name of the macOS package signing key name that you'd like to use to sign the macOS packages. For example: @@ -61,7 +63,7 @@ You can find various ways to install osquery on a variety of platforms at https: #### Set an environment variable with an agent enrollment secret -The enrollment secret is a value that osquery uses to ensure a level of confidence that the host running osquery is actually a host that you would like to hear from. There are a few ways you can set the enrollment secret on the hosts which you control. You can either set the value as: +The enrollment secret is a value that osquery provides to authenticate with Fleet. There are a few ways you can set the enrollment secret on the hosts which you control. You can either set the value as: - an value of an environment variable (a common name is `OSQUERY_ENROLL_SECRET`) - the content of a local file (a common path is `/etc/osquery/enrollment_secret`) @@ -69,7 +71,10 @@ The enrollment secret is a value that osquery uses to ensure a level of confiden The value of the environment variable or content of the file should be a secret shared between the osqueryd client and the Fleet server. This is basically osqueryd's passphrase which it uses to authenticate with Fleet, convincing Fleet that it is actually one of your hosts. The passphrase could be whatever you'd like, but it would be prudent to have the passphrase long, complex, mixed-case, etc. When you launch the Fleet server, you should specify this same value. If you use an environment variable for this, you can specify it with the `--enroll_secret_env` flag when you launch osqueryd. If you use a local file for this, you can specify it's path with the `--enroll_secret_path` flag. -s + +To retrieve the enroll secret, use the "Add New Host" dialog in the Fleet UI or +`fleetctl get enroll_secret`). + If your organization has a robust internal public key infrastructure (PKI) and you already deploy TLS client certificates to each host to uniquely identify them, then osquery supports an advanced authentication mechanism which takes advantage of this. Fleet can be fronted with a proxy that will perform the TLS client authentication. #### Deploy the TLS certificate that osquery will use to communicate with Fleet @@ -140,3 +145,11 @@ Note that osqueryd requires a full certificate chain, even for certificates whic Once you've configured the `config.mk` file with the correct variables, you can run `make` in the `tools/mac` directory. Running `make` will create a new `kolide-enroll.pkg` file which you can import into your software repository and deploy to your mac fleet. The enrollment package must installed after the osqueryd package, and will install a LaunchDaemon to keep the osqueryd process running. + +## Multiple Enroll Secrets + +Multiple enroll secrets can be set to allow different groups of hosts to +authenticate with Fleet. When a host enrolls, the corresponding enroll secret is +recorded and can be used to segment hosts. + +To set the enroll secret, use the `fleetctl` tool to apply an [enroll secret spec](../cli/file-format.md#enroll-secrets) diff --git a/examples/config-many-files/enroll-secret.yml b/examples/config-many-files/enroll-secret.yml new file mode 100644 index 000000000..796e78daf --- /dev/null +++ b/examples/config-many-files/enroll-secret.yml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: enroll_secret +spec: + secrets: + - active: true + name: default + secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff + - active: true + name: new_one + secret: reallyworks + - active: false + name: inactive_secret + secret: thissecretwontwork! diff --git a/examples/config-single-file.yml b/examples/config-single-file.yml index 29fc23246..01e4a6a83 100644 --- a/examples/config-single-file.yml +++ b/examples/config-single-file.yml @@ -69,6 +69,20 @@ spec: 3600: "SELECT total_seconds AS uptime FROM uptime" --- apiVersion: v1 +kind: enroll_secret +spec: + secrets: + - active: true + name: default + secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff + - active: true + name: new_one + secret: reallyworks + - active: false + name: inactive_secret + secret: thissecretwontwork! +--- +apiVersion: v1 kind: label spec: name: pending_updates diff --git a/frontend/components/App/App.jsx b/frontend/components/App/App.jsx index 490715dc6..38f4e6681 100644 --- a/frontend/components/App/App.jsx +++ b/frontend/components/App/App.jsx @@ -6,7 +6,7 @@ import classnames from 'classnames'; import { authToken } from 'utilities/local'; import { fetchCurrentUser } from 'redux/nodes/auth/actions'; -import { getConfig } from 'redux/nodes/app/actions'; +import { getConfig, getEnrollSecret } from 'redux/nodes/app/actions'; import userInterface from 'interfaces/user'; export class App extends Component { @@ -32,6 +32,8 @@ export class App extends Component { if (user) { dispatch(getConfig()) .catch(() => false); + dispatch(getEnrollSecret()) + .catch(() => false); } return false; @@ -43,6 +45,8 @@ export class App extends Component { if (user && this.props.user !== user) { dispatch(getConfig()) .catch(() => false); + dispatch(getEnrollSecret()) + .catch(() => false); } } diff --git a/frontend/components/config/EnrollSecretTable/EnrollSecretTable.jsx b/frontend/components/config/EnrollSecretTable/EnrollSecretTable.jsx new file mode 100644 index 000000000..87824a659 --- /dev/null +++ b/frontend/components/config/EnrollSecretTable/EnrollSecretTable.jsx @@ -0,0 +1,122 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import Button from 'components/buttons/Button'; +import enrollSecretInterface from 'interfaces/enroll_secret'; +import InputField from 'components/forms/fields/InputField'; +import Icon from 'components/icons/Icon'; +import { stringToClipboard } from 'utilities/copy_text'; + +const baseClass = 'enroll-secrets'; + +class EnrollSecretRow extends Component { + static propTypes = { + name: PropTypes.string.isRequired, + secret: PropTypes.string.isRequired, + } + + constructor (props) { + super(props); + this.state = { showSecret: false, copyMessage: '' }; + } + + onCopySecret = (evt) => { + evt.preventDefault(); + + const { secret } = this.props; + + stringToClipboard(secret) + .then(() => this.setState({ copyMessage: '(copied)' })) + .catch(() => this.setState({ copyMessage: '(copy failed)' })); + + // Clear message after 1 second + setTimeout(() => this.setState({ copyMessage: '' }), 1000); + + return false; + } + + onToggleSecret = (evt) => { + evt.preventDefault(); + + const { showSecret } = this.state; + + this.setState({ showSecret: !showSecret }); + return false; + }; + + renderLabel = () => { + const { name } = this.props; + const { showSecret, copyMessage } = this.state; + const { onCopySecret, onToggleSecret } = this; + + return ( + + {name} + + {copyMessage && {`${copyMessage} `}} + + + {showSecret ? 'Hide' : 'Show'} + + + + ); + } + + render () { + const { secret } = this.props; + const { showSecret } = this.state; + const { renderLabel } = this; + + return ( +
+ +
+ ); + } + +} + +class EnrollSecretTable extends Component { + static propTypes = { + secrets: enrollSecretInterface.isRequired, + } + + render() { + const { secrets } = this.props; + const activeSecrets = secrets.filter(s => s.active); + + if (activeSecrets.length === 0) { + return (
No active enroll secrets.
); + } + + return ( +
+ {activeSecrets.map(({ name, secret }) => + + )} +
+ ); + } + +} + +export default EnrollSecretTable; +export { EnrollSecretRow }; diff --git a/frontend/components/config/EnrollSecretTable/EnrollSecretTable.tests.jsx b/frontend/components/config/EnrollSecretTable/EnrollSecretTable.tests.jsx new file mode 100644 index 000000000..cc27f460d --- /dev/null +++ b/frontend/components/config/EnrollSecretTable/EnrollSecretTable.tests.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import expect, { spyOn } from 'expect'; +import { shallow, mount } from 'enzyme'; + +import * as copy from 'utilities/copy_text'; +import EnrollSecretTable, { EnrollSecretRow } from 'components/config/EnrollSecretTable/EnrollSecretTable'; + +describe('EnrollSecretTable', () => { + const defaultProps = { + secrets: [ + { name: 'foo', secret: 'foo_secret', active: true }, + { name: 'bar', secret: 'bar_secret', active: true }, + { name: 'inactive', secret: 'inactive', active: false }, + ], + }; + + it('renders properly filtered rows', () => { + const table = shallow(); + expect(table.find('EnrollSecretRow').length).toEqual(2); + }); + + it('renders text when empty', () => { + const table = shallow(); + expect(table.find('EnrollSecretRow').length).toEqual(0); + expect(table.find('div').text()).toEqual('No active enroll secrets.'); + }); +}); + +describe('EnrollSecretRow', () => { + const defaultProps = { name: 'foo', secret: 'bar' }; + it('should hide secret by default', () => { + const row = mount(); + const inputField = row.find('InputField').find('input'); + expect(inputField.prop('type')).toEqual('password'); + }); + + it('should show secret when enabled', () => { + const row = mount(); + row.setState({ showSecret: true }); + const inputField = row.find('InputField').find('input'); + expect(inputField.prop('type')).toEqual('text'); + }); + + it('should change input type when show/hide is clicked', () => { + const row = mount(); + + let inputField = row.find('InputField').find('input'); + expect(inputField.prop('type')).toEqual('password'); + + const showLink = row.find('.enroll-secrets__show-secret'); + expect(showLink.text()).toEqual('Show'); + + showLink.simulate('click'); + + inputField = row.find('InputField').find('input'); + expect(inputField.prop('type')).toEqual('text'); + + const hideLink = row.find('.enroll-secrets__show-secret'); + expect(hideLink.text()).toEqual('Hide'); + + hideLink.simulate('click'); + + inputField = row.find('InputField').find('input'); + expect(inputField.prop('type')).toEqual('password'); + }); + + it('should call copy when button is clicked', () => { + const row = mount(); + const spy = spyOn(copy, 'stringToClipboard').andReturn(Promise.resolve()); + + const copyLink = row.find('.enroll-secrets__secret-copy-icon').find('Button'); + copyLink.simulate('click'); + + expect(spy).toHaveBeenCalledWith(defaultProps.secret); + }); +}); diff --git a/frontend/components/config/EnrollSecretTable/_styles.scss b/frontend/components/config/EnrollSecretTable/_styles.scss new file mode 100644 index 000000000..8941d241e --- /dev/null +++ b/frontend/components/config/EnrollSecretTable/_styles.scss @@ -0,0 +1,9 @@ +.enroll-secrets { + max-height: 10em; + overflow: auto; + + + .buttons { + float: right; + } +} diff --git a/frontend/components/config/EnrollSecretTable/index.js b/frontend/components/config/EnrollSecretTable/index.js new file mode 100644 index 000000000..db9495960 --- /dev/null +++ b/frontend/components/config/EnrollSecretTable/index.js @@ -0,0 +1 @@ +export default from './EnrollSecretTable'; diff --git a/frontend/components/forms/FormField/FormField.jsx b/frontend/components/forms/FormField/FormField.jsx index 4072ade9a..3901c27b1 100644 --- a/frontend/components/forms/FormField/FormField.jsx +++ b/frontend/components/forms/FormField/FormField.jsx @@ -10,7 +10,7 @@ class FormField extends Component { className: PropTypes.string, error: PropTypes.string, hint: PropTypes.oneOfType([PropTypes.array, PropTypes.node, PropTypes.string]), - label: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), + label: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.node]), name: PropTypes.string, type: PropTypes.string, }; diff --git a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx index 2ba9d1f82..325512f51 100644 --- a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx +++ b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx @@ -6,6 +6,8 @@ import Checkbox from 'components/forms/fields/Checkbox'; import Dropdown from 'components/forms/fields/Dropdown'; import Form from 'components/forms/Form'; import formFieldInterface from 'interfaces/form_field'; +import enrollSecretInterface from 'interfaces/enroll_secret'; +import EnrollSecretTable from 'components/config/EnrollSecretTable'; import Icon from 'components/icons/Icon'; import InputField from 'components/forms/fields/InputField'; import OrgLogoIcon from 'components/icons/OrgLogoIcon'; @@ -66,6 +68,7 @@ class AppConfigForm extends Component { host_expiry_window: formFieldInterface.isRequired, live_query_disabled: formFieldInterface.isRequired, }).isRequired, + enrollSecret: enrollSecretInterface.isRequired, handleSubmit: PropTypes.func.isRequired, smtpConfigured: PropTypes.bool.isRequired, }; @@ -73,7 +76,7 @@ class AppConfigForm extends Component { constructor (props) { super(props); - this.state = { revealSecret: false, showAdvancedOptions: false }; + this.state = { showAdvancedOptions: false }; } onToggleAdvancedOptions = (evt) => { @@ -86,16 +89,6 @@ class AppConfigForm extends Component { return false; } - onToggleRevealSecret = (evt) => { - evt.preventDefault(); - - const { revealSecret } = this.state; - - this.setState({ revealSecret: !revealSecret }); - - return false; - } - renderAdvancedOptions = () => { const { fields } = this.props; const { showAdvancedOptions } = this.state; @@ -158,9 +151,9 @@ class AppConfigForm extends Component { } render () { - const { fields, handleSubmit, smtpConfigured } = this.props; - const { onToggleAdvancedOptions, onToggleRevealSecret, renderAdvancedOptions, renderSmtpSection } = this; - const { revealSecret, showAdvancedOptions } = this.state; + const { fields, handleSubmit, smtpConfigured, enrollSecret } = this.props; + const { onToggleAdvancedOptions, renderAdvancedOptions, renderSmtpSection } = this; + const { showAdvancedOptions } = this.state; return (
@@ -324,16 +317,12 @@ class AppConfigForm extends Component {
-

Osquery Enrollment Secret

+

Osquery Enrollment Secrets

- This is the secret that you use to enroll osquery agents with Fleet: - + Manage secrets with fleetctl. Active secrets:

- +
diff --git a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.tests.jsx b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.tests.jsx index 2e5bce61c..2e9f12357 100644 --- a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.tests.jsx +++ b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.tests.jsx @@ -11,6 +11,11 @@ describe('AppConfigForm - form', () => { formData: { org_name: 'Kolide' }, handleSubmit: noop, smtpConfigured: false, + enrollSecret: [ + { name: 'foo', secret: 'foo_secret', active: true }, + { name: 'bar', secret: 'bar_secret', active: true }, + { name: 'inactive', secret: 'inactive', active: false }, + ], }; const form = mount(); @@ -74,6 +79,13 @@ describe('AppConfigForm - form', () => { }); }); + describe('Enroll Secret', () => { + it('renders enroll secrets table', () => { + expect(form.find('EnrollSecretTable').length).toEqual(1); + }); + }); + + it('does not render advanced options by default', () => { expect(form.find({ name: 'domain' }).length).toEqual(0); expect(form.find('Slider').length).toEqual(0); diff --git a/frontend/components/hosts/AddHostModal/AddHostModal.jsx b/frontend/components/hosts/AddHostModal/AddHostModal.jsx index 36c649750..9c541c1a9 100644 --- a/frontend/components/hosts/AddHostModal/AddHostModal.jsx +++ b/frontend/components/hosts/AddHostModal/AddHostModal.jsx @@ -2,62 +2,25 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Button from 'components/buttons/Button'; +import enrollSecretInterface from 'interfaces/enroll_secret'; +import EnrollSecretTable from 'components/config/EnrollSecretTable'; import Icon from 'components/icons/Icon'; -import InputField from 'components/forms/fields/InputField'; -import { renderFlash } from 'redux/nodes/notifications/actions'; -import { - copyText, - COPY_TEXT_SUCCESS, - COPY_TEXT_ERROR, -} from 'utilities/copy_text'; import certificate from '../../../../assets/images/osquery-certificate.svg'; const baseClass = 'add-host-modal'; class AddHostModal extends Component { static propTypes = { - dispatch: PropTypes.func, onFetchCertificate: PropTypes.func, onReturnToApp: PropTypes.func, - osqueryEnrollSecret: PropTypes.string, - }; - - constructor(props) { - super(props); - - this.state = { revealSecret: false }; - } - - onCopySecret = (elementClass) => { - return (evt) => { - evt.preventDefault(); - - const { dispatch } = this.props; - - if (copyText(elementClass)) { - dispatch(renderFlash('success', COPY_TEXT_SUCCESS)); - } else { - this.setState({ revealSecret: true }); - dispatch(renderFlash('error', COPY_TEXT_ERROR)); - } - }; - }; - - toggleSecret = (evt) => { - const { revealSecret } = this.state; - evt.preventDefault(); - - this.setState({ revealSecret: !revealSecret }); - return false; + enrollSecret: enrollSecretInterface, }; render() { - const { onCopySecret, toggleSecret } = this; - const { revealSecret } = this.state; const { onFetchCertificate, onReturnToApp, - osqueryEnrollSecret, + enrollSecret, } = this.props; return ( @@ -66,14 +29,6 @@ class AddHostModal extends Component { Follow the instructions below to add hosts to your Fleet Instance.

-
- -

Manual Install

-

- Fully Customize Your Osquery Installation -

-
-
  1. @@ -86,38 +41,14 @@ class AddHostModal extends Component { Fleet / Osquery - Install Docs -

    - In order to install osquery on a client you - will need the following information: -

  2. -

    Retrieve Osquery Enroll Secret

    +

    Osquery Enroll Secret

    - The following is your enroll secret: - - {revealSecret ? 'Hide' : 'Reveal'} Secret - + Provide osquery with one of the following active enroll secrets:

    - - +
  3. diff --git a/frontend/components/hosts/AddHostModal/AddHostModal.tests.jsx b/frontend/components/hosts/AddHostModal/AddHostModal.tests.jsx deleted file mode 100644 index 0e20b57fe..000000000 --- a/frontend/components/hosts/AddHostModal/AddHostModal.tests.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import expect from 'expect'; -import { mount } from 'enzyme'; -import { noop } from 'lodash'; - -import AddHostModal from './AddHostModal'; - -describe('AddHostModal - component', () => { - it('clicking Reveal Secret should change input type', () => { - const component = mount(); - const revealSecretLink = component.find('.add-host-modal__reveal-secret'); - let secretInput = component.find('.add-host-modal__secret-input').find('input'); - - expect(component.state().revealSecret).toEqual(false); - expect(secretInput.prop('type')).toEqual('password'); - expect(revealSecretLink.text()).toEqual('Reveal Secret'); - - revealSecretLink.simulate('click'); - - secretInput = component.find('.add-host-modal__secret-input').find('input'); - expect(component.state().revealSecret).toEqual(true); - expect(secretInput.prop('type')).toEqual('text'); - expect(revealSecretLink.text()).toEqual('Hide Secret'); - - revealSecretLink.simulate('click'); - - secretInput = component.find('.add-host-modal__secret-input').find('input'); - expect(component.state().revealSecret).toEqual(false); - expect(secretInput.prop('type')).toEqual('password'); - expect(revealSecretLink.text()).toEqual('Reveal Secret'); - }); -}); diff --git a/frontend/interfaces/enroll_secret.js b/frontend/interfaces/enroll_secret.js new file mode 100644 index 000000000..2694851f3 --- /dev/null +++ b/frontend/interfaces/enroll_secret.js @@ -0,0 +1,10 @@ +import PropTypes from 'prop-types'; + +export default PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + secret: PropTypes.string, + active: PropTypes.bool, + created_at: PropTypes.string, + }), +); diff --git a/frontend/kolide/entities/config.js b/frontend/kolide/entities/config.js index 012e9c987..ad645ad56 100644 --- a/frontend/kolide/entities/config.js +++ b/frontend/kolide/entities/config.js @@ -16,6 +16,11 @@ export default (client) => { return client.authenticatedGet(endpoint) .then(response => global.window.atob(response.certificate_chain)); }, + loadEnrollSecret: () => { + const endpoint = client._endpoint('/v1/kolide/spec/enroll_secret'); + + return client.authenticatedGet(endpoint); + }, update: (formData) => { const { CONFIG } = endpoints; const configData = helpers.formatConfigDataForServer(formData); diff --git a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.jsx b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.jsx index 03478f85f..e923652f2 100644 --- a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.jsx +++ b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.jsx @@ -5,6 +5,7 @@ import { size } from 'lodash'; import AppConfigForm from 'components/forms/admin/AppConfigForm'; import configInterface from 'interfaces/config'; +import enrollSecretInterface from 'interfaces/enroll_secret'; import deepDifference from 'utilities/deep_difference'; import { renderFlash } from 'redux/nodes/notifications/actions'; import WarningBanner from 'components/WarningBanner'; @@ -17,6 +18,7 @@ class AppSettingsPage extends Component { appConfig: configInterface, dispatch: PropTypes.func.isRequired, error: PropTypes.object, // eslint-disable-line react/forbid-prop-types + enrollSecret: enrollSecretInterface, }; constructor (props) { @@ -53,7 +55,7 @@ class AppSettingsPage extends Component { } render () { - const { appConfig, error } = this.props; + const { appConfig, error, enrollSecret } = this.props; const { onDismissSmtpWarning, onFormSubmit } = this; const { showSmtpWarning } = this.state; const { configured: smtpConfigured } = appConfig; @@ -78,6 +80,7 @@ class AppSettingsPage extends Component { handleSubmit={onFormSubmit} serverErrors={error} smtpConfigured={smtpConfigured} + enrollSecret={enrollSecret} />
); @@ -85,9 +88,9 @@ class AppSettingsPage extends Component { } const mapStateToProps = ({ app }) => { - const { config: appConfig, error } = app; + const { config: appConfig, error, enrollSecret } = app; - return { appConfig, error }; + return { appConfig, error, enrollSecret }; }; export default connect(mapStateToProps)(AppSettingsPage); diff --git a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tests.jsx b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tests.jsx index 1ed8d5899..03227a4aa 100644 --- a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tests.jsx +++ b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tests.jsx @@ -7,10 +7,22 @@ import testHelpers from 'test/helpers'; const { connectedComponent, reduxMockStore } = testHelpers; const baseStore = { - app: { config: flatConfigStub }, + app: { config: flatConfigStub, enrollSecret: [] }, +}; +const storeWithoutSMTPConfig = { + ...baseStore, + app: { + config: { ...flatConfigStub, configured: false }, + enrollSecret: [], + }, +}; +const storeWithSMTPConfig = { + ...baseStore, + app: { + config: { ...flatConfigStub, configured: true }, + enrollSecret: [], + }, }; -const storeWithoutSMTPConfig = { ...baseStore, app: { config: { ...flatConfigStub, configured: false } } }; -const storeWithSMTPConfig = { ...baseStore, app: { config: { ...flatConfigStub, configured: true } } }; describe('AppSettingsPage - component', () => { afterEach(restoreSpies); diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx index c6e59eee4..99d027c66 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx @@ -22,6 +22,7 @@ import labelInterface from 'interfaces/label'; import hostInterface from 'interfaces/host'; import osqueryTableInterface from 'interfaces/osquery_table'; import statusLabelsInterface from 'interfaces/status_labels'; +import enrollSecretInterface from 'interfaces/enroll_secret'; import { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions'; import { getStatusLabelCounts, setDisplay, silentGetStatusLabelCounts } from 'redux/nodes/components/ManageHostsPage/actions'; import hostActions from 'redux/nodes/entities/hosts/actions'; @@ -51,7 +52,7 @@ export class ManageHostsPage extends PureComponent { labels: PropTypes.arrayOf(labelInterface), loadingHosts: PropTypes.bool.isRequired, loadingLabels: PropTypes.bool.isRequired, - osqueryEnrollSecret: PropTypes.string, + enrollSecret: enrollSecretInterface, selectedLabel: labelInterface, selectedOsqueryTable: osqueryTableInterface, statusLabels: statusLabelsInterface, @@ -356,7 +357,7 @@ export class ManageHostsPage extends PureComponent { renderAddHostModal = () => { const { onFetchCertificate, toggleAddHostModal } = this; const { showAddHostModal } = this.state; - const { dispatch, osqueryEnrollSecret } = this.props; + const { enrollSecret } = this.props; if (!showAddHostModal) { return false; @@ -369,10 +370,9 @@ export class ManageHostsPage extends PureComponent { className={`${baseClass}__invite-modal`} > ); @@ -676,7 +676,7 @@ const mapStateToProps = (state, { location, params }) => { const { selectedOsqueryTable } = state.components.QueryPages; const { errors: labelErrors, loading: loadingLabels } = state.entities.labels; const { loading: loadingHosts } = state.entities.hosts; - const { osquery_enroll_secret: osqueryEnrollSecret } = state.app.config; + const enrollSecret = state.app.enrollSecret; return { display, @@ -686,7 +686,7 @@ const mapStateToProps = (state, { location, params }) => { labels, loadingHosts, loadingLabels, - osqueryEnrollSecret, + enrollSecret, selectedLabel, selectedOsqueryTable, statusLabels, diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx index fcb2dad3e..22612ea8e 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx @@ -56,6 +56,7 @@ describe('ManageHostsPage - component', () => { loadingLabels: false, selectedOsqueryTable: stubbedOsqueryTable, statusLabels: {}, + enrollSecret: [], }; beforeEach(() => { diff --git a/frontend/redux/nodes/app/actions.js b/frontend/redux/nodes/app/actions.js index 043a09699..f798315cd 100644 --- a/frontend/redux/nodes/app/actions.js +++ b/frontend/redux/nodes/app/actions.js @@ -6,6 +6,9 @@ import { frontendFormattedConfig } from 'redux/nodes/app/helpers'; export const CONFIG_FAILURE = 'CONFIG_FAILURE'; export const CONFIG_START = 'CONFIG_START'; export const CONFIG_SUCCESS = 'CONFIG_SUCCESS'; +export const ENROLL_SECRET_FAILURE = 'ENROLL_SECRET_FAILURE'; +export const ENROLL_SECRET_START = 'ENROLL_SECRET_START'; +export const ENROLL_SECRET_SUCCESS = 'ENROLL_SECRET_SUCCESS'; export const SHOW_BACKGROUND_IMAGE = 'SHOW_BACKGROUND_IMAGE'; export const HIDE_BACKGROUND_IMAGE = 'HIDE_BACKGROUND_IMAGE'; export const TOGGLE_SMALL_NAV = 'TOGGLE_SMALL_NAV'; @@ -26,6 +29,13 @@ export const loadConfig = { type: CONFIG_START }; export const configSuccess = (data) => { return { type: CONFIG_SUCCESS, payload: { data } }; }; +export const enrollSecretFailure = (error) => { + return { type: ENROLL_SECRET_FAILURE, payload: { error } }; +}; +export const loadEnrollSecret = { type: ENROLL_SECRET_START }; +export const enrollSecretSuccess = (data) => { + return { type: ENROLL_SECRET_SUCCESS, payload: { data } }; +}; export const getConfig = () => { return (dispatch) => { dispatch(loadConfig); @@ -67,3 +77,22 @@ export const updateConfig = (configData) => { }); }; }; +export const getEnrollSecret = () => { + return (dispatch) => { + dispatch(loadEnrollSecret); + + return Kolide.config.loadEnrollSecret() + .then((secret) => { + dispatch(enrollSecretSuccess(secret.specs.secrets)); + + return secret; + }) + .catch((error) => { + const formattedErrors = formatApiErrors(error); + + dispatch(enrollSecretFailure(formattedErrors)); + + throw formattedErrors; + }); + }; +}; diff --git a/frontend/redux/nodes/app/actions.tests.js b/frontend/redux/nodes/app/actions.tests.js index 8f2056dc9..3a0d2c121 100644 --- a/frontend/redux/nodes/app/actions.tests.js +++ b/frontend/redux/nodes/app/actions.tests.js @@ -1,6 +1,14 @@ import expect from 'expect'; -import { CONFIG_START, CONFIG_SUCCESS, getConfig, updateConfig } from 'redux/nodes/app/actions'; +import { + CONFIG_START, + CONFIG_SUCCESS, + ENROLL_SECRET_START, + ENROLL_SECRET_SUCCESS, + getConfig, + updateConfig, + getEnrollSecret, +} from 'redux/nodes/app/actions'; import { configStub } from 'test/stubs'; import { frontendFormattedConfig } from 'redux/nodes/app/helpers'; import Kolide from 'kolide'; @@ -78,4 +86,38 @@ describe('App - actions', () => { .catch(done); }); }); + + describe('getEnrollSecret action', () => { + const store = reduxMockStore({}); + + it('calls the api enrollSecret endpoint', (done) => { + const bearerToken = 'abc123'; + const request = configMocks.loadAll.valid(bearerToken); + + Kolide.setBearerToken(bearerToken); + store.dispatch(getEnrollSecret()) + .then(() => { + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(done); + }); + + it('dispatches ENROLLSECRET_START & ENROLLSECRET_SUCCESS actions', (done) => { + const bearerToken = 'abc123'; + configMocks.loadAll.valid(bearerToken); + + Kolide.setBearerToken(bearerToken); + store.dispatch(getEnrollSecret()) + .then(() => { + const actions = store.getActions() + .map((action) => { return action.type; }); + + expect(actions).toInclude(ENROLL_SECRET_START); + expect(actions).toInclude(ENROLL_SECRET_SUCCESS); + done(); + }) + .catch(done); + }); + }); }); diff --git a/frontend/redux/nodes/app/reducer.js b/frontend/redux/nodes/app/reducer.js index 293509d9a..b8cc9d43a 100644 --- a/frontend/redux/nodes/app/reducer.js +++ b/frontend/redux/nodes/app/reducer.js @@ -2,6 +2,9 @@ import { CONFIG_FAILURE, CONFIG_START, CONFIG_SUCCESS, + ENROLL_SECRET_FAILURE, + ENROLL_SECRET_START, + ENROLL_SECRET_SUCCESS, HIDE_BACKGROUND_IMAGE, SHOW_BACKGROUND_IMAGE, TOGGLE_SMALL_NAV, @@ -9,6 +12,7 @@ import { export const initialState = { config: {}, + enrollSecret: [], error: {}, isSmallNav: false, loading: false, @@ -35,6 +39,24 @@ const reducer = (state = initialState, { type, payload }) => { error: payload.error, loading: false, }; + case ENROLL_SECRET_START: + return { + ...state, + loading: true, + }; + case ENROLL_SECRET_SUCCESS: + return { + ...state, + enrollSecret: payload.data, + error: {}, + loading: false, + }; + case ENROLL_SECRET_FAILURE: + return { + ...state, + error: payload.error, + loading: false, + }; case HIDE_BACKGROUND_IMAGE: return { ...state, diff --git a/frontend/redux/nodes/app/reducer.tests.js b/frontend/redux/nodes/app/reducer.tests.js index d6016c4aa..64b0808a9 100644 --- a/frontend/redux/nodes/app/reducer.tests.js +++ b/frontend/redux/nodes/app/reducer.tests.js @@ -2,12 +2,15 @@ import expect from 'expect'; import reducer, { initialState } from './reducer'; import { + loadConfig, configFailure, configSuccess, + loadEnrollSecret, + enrollSecretFailure, + enrollSecretSuccess, hideBackgroundImage, showBackgroundImage, toggleSmallNav, - loadConfig, } from './actions'; describe('App - reducer', () => { @@ -76,6 +79,7 @@ describe('App - reducer', () => { }; expect(reducer(loadingConfigState, configSuccess(config))).toEqual({ config, + enrollSecret: [], error: {}, loading: false, isSmallNav: false, @@ -92,6 +96,52 @@ describe('App - reducer', () => { loading: true, }; expect(reducer(loadingConfigState, configFailure(error))).toEqual({ + config: {}, + enrollSecret: [], + error, + loading: false, + isSmallNav: false, + showBackgroundImage: false, + }); + }); + }); + + context('loadEnrollSecret action', () => { + it('sets the state to loading', () => { + expect(reducer(initialState, loadEnrollSecret)).toEqual({ + ...initialState, + loading: true, + }); + }); + }); + + context('enrollSecretSuccess action', () => { + it('sets the enrollSecret in state', () => { + const enrollSecret = [{ name: 'Kolide' }]; + const loadingEnrollSecretState = { + ...initialState, + loading: true, + }; + expect(reducer(loadingEnrollSecretState, enrollSecretSuccess(enrollSecret))).toEqual({ + enrollSecret, + config: {}, + error: {}, + loading: false, + isSmallNav: false, + showBackgroundImage: false, + }); + }); + }); + + context('enrollSecretFailure action', () => { + it('sets the error in state', () => { + const error = 'Unable to get enrollSecret'; + const loadingEnrollSecretState = { + ...initialState, + loading: true, + }; + expect(reducer(loadingEnrollSecretState, enrollSecretFailure(error))).toEqual({ + enrollSecret: [], config: {}, error, loading: false, diff --git a/frontend/utilities/copy_text.js b/frontend/utilities/copy_text.js index 10b1fa03e..7bd1777ea 100644 --- a/frontend/utilities/copy_text.js +++ b/frontend/utilities/copy_text.js @@ -27,6 +27,12 @@ export const copyText = (elementSelector) => { return true; }; +export const stringToClipboard = (string) => { + const { navigator } = global; + + return navigator.clipboard.writeText(string); +}; + export const COPY_TEXT_SUCCESS = 'Text copied to clipboard'; export const COPY_TEXT_ERROR = 'Text not copied. Please copy manually.'; diff --git a/server/datastore/datastore_app_test.go b/server/datastore/datastore_app_test.go index de50f1023..587e12c46 100644 --- a/server/datastore/datastore_app_test.go +++ b/server/datastore/datastore_app_test.go @@ -2,6 +2,7 @@ package datastore import ( "encoding/json" + "sort" "testing" "github.com/kolide/fleet/server/kolide" @@ -75,3 +76,78 @@ func testAdditionalQueries(t *testing.T, ds kolide.Datastore) { assert.Nil(t, err) assert.JSONEq(t, `{"foo":"bar"}`, string(*info.AdditionalQueries)) } + +func testEnrollSecrets(t *testing.T, ds kolide.Datastore) { + name, err := ds.VerifyEnrollSecret("missing") + assert.Error(t, err) + assert.Empty(t, name) + + err = ds.ApplyEnrollSecretSpec( + &kolide.EnrollSecretSpec{ + Secrets: []kolide.EnrollSecret{ + kolide.EnrollSecret{Name: "one", Secret: "one_secret", Active: true}, + kolide.EnrollSecret{Name: "two", Secret: "two_secret", Active: false}, + }, + }, + ) + assert.NoError(t, err) + + name, err = ds.VerifyEnrollSecret("one") + assert.Error(t, err, "secret should not match") + assert.Empty(t, name, "secret name should be empty") + name, err = ds.VerifyEnrollSecret("one_secret") + assert.NoError(t, err) + assert.Equal(t, "one", name) + name, err = ds.VerifyEnrollSecret("two_secret") + assert.Error(t, err) + assert.Equal(t, "", name) + + err = ds.ApplyEnrollSecretSpec( + &kolide.EnrollSecretSpec{ + Secrets: []kolide.EnrollSecret{ + kolide.EnrollSecret{Name: "one", Secret: "one_secret", Active: false}, + kolide.EnrollSecret{Name: "two", Secret: "two_secret", Active: true}, + }, + }, + ) + assert.NoError(t, err) + + name, err = ds.VerifyEnrollSecret("one_secret") + assert.Error(t, err) + assert.Equal(t, "", name) + name, err = ds.VerifyEnrollSecret("two_secret") + assert.NoError(t, err) + assert.Equal(t, "two", name) + +} + +func testEnrollSecretRoundtrip(t *testing.T, ds kolide.Datastore) { + spec, err := ds.GetEnrollSecretSpec() + require.NoError(t, err) + assert.Len(t, spec.Secrets, 1) + + expectedSpec := kolide.EnrollSecretSpec{ + Secrets: []kolide.EnrollSecret{ + kolide.EnrollSecret{Name: "one", Secret: "one_secret", Active: false}, + kolide.EnrollSecret{Name: "two", Secret: "two_secret", Active: true}, + }, + } + err = ds.ApplyEnrollSecretSpec(&expectedSpec) + require.NoError(t, err) + + spec, err = ds.GetEnrollSecretSpec() + require.NoError(t, err) + require.Len(t, spec.Secrets, 3) + // sort secrets before equality checks to ensure proper order + sort.Slice(spec.Secrets, func(i, j int) bool { return spec.Secrets[i].Name < spec.Secrets[j].Name }) + + assert.Equal(t, "default", spec.Secrets[0].Name) + + assert.Equal(t, "one", spec.Secrets[1].Name) + assert.Equal(t, "one_secret", spec.Secrets[1].Secret) + assert.Equal(t, false, spec.Secrets[1].Active) + + assert.Equal(t, "two", spec.Secrets[2].Name) + assert.Equal(t, "two_secret", spec.Secrets[2].Secret) + assert.Equal(t, true, spec.Secrets[2].Active) +} diff --git a/server/datastore/datastore_hosts_test.go b/server/datastore/datastore_hosts_test.go index 7d730c00e..d90b22c4a 100644 --- a/server/datastore/datastore_hosts_test.go +++ b/server/datastore/datastore_hosts_test.go @@ -16,28 +16,27 @@ import ( ) var enrollTests = []struct { - uuid, hostname, platform string - nodeKeySize int + uuid, hostname, platform, nodeKey string }{ 0: {uuid: "6D14C88F-8ECF-48D5-9197-777647BF6B26", - hostname: "web.kolide.co", - platform: "linux", - nodeKeySize: 12, + hostname: "web.kolide.co", + platform: "linux", + nodeKey: "key0", }, 1: {uuid: "B998C0EB-38CE-43B1-A743-FBD7A5C9513B", - hostname: "mail.kolide.co", - platform: "linux", - nodeKeySize: 10, + hostname: "mail.kolide.co", + platform: "linux", + nodeKey: "key1", }, 2: {uuid: "008F0688-5311-4C59-86EE-00C2D6FC3EC2", - hostname: "home.kolide.co", - platform: "darwin", - nodeKeySize: 25, + hostname: "home.kolide.co", + platform: "darwin", + nodeKey: "key2", }, 3: {uuid: "uuid123", - hostname: "fakehostname", - platform: "darwin", - nodeKeySize: 1, + hostname: "fakehostname", + platform: "darwin", + nodeKey: "key3", }, } @@ -259,7 +258,7 @@ func testListHost(t *testing.T, ds kolide.Datastore) { func testEnrollHost(t *testing.T, ds kolide.Datastore) { var hosts []*kolide.Host for _, tt := range enrollTests { - h, err := ds.EnrollHost(tt.uuid, tt.nodeKeySize) + h, err := ds.EnrollHost(tt.uuid, tt.nodeKey, "default") require.Nil(t, err) hosts = append(hosts, h) @@ -271,7 +270,7 @@ func testEnrollHost(t *testing.T, ds kolide.Datastore) { func testAuthenticateHost(t *testing.T, ds kolide.Datastore) { for _, tt := range enrollTests { - h, err := ds.EnrollHost(tt.uuid, tt.nodeKeySize) + h, err := ds.EnrollHost(tt.uuid, tt.nodeKey, "default") require.Nil(t, err) returned, err := ds.AuthenticateHost(h.NodeKey) diff --git a/server/datastore/datastore_labels_test.go b/server/datastore/datastore_labels_test.go index c442c1149..57456beac 100644 --- a/server/datastore/datastore_labels_test.go +++ b/server/datastore/datastore_labels_test.go @@ -17,7 +17,7 @@ func testLabels(t *testing.T, db kolide.Datastore) { var host *kolide.Host var err error for i := 0; i < 10; i++ { - host, err = db.EnrollHost(string(i), 10) + host, err = db.EnrollHost(string(i), string(i), "default") require.Nil(t, err, "enrollment should succeed") hosts = append(hosts, *host) } diff --git a/server/datastore/datastore_targets_test.go b/server/datastore/datastore_targets_test.go index 4f97b87ff..b68c53242 100644 --- a/server/datastore/datastore_targets_test.go +++ b/server/datastore/datastore_targets_test.go @@ -126,7 +126,7 @@ func testHostStatus(t *testing.T, ds kolide.Datastore) { mockClock := clock.NewMockClock() - h, err := ds.EnrollHost("1", 24) + h, err := ds.EnrollHost("1", "key1", "default") require.Nil(t, err) // Make host no longer appear new diff --git a/server/datastore/datastore_test.go b/server/datastore/datastore_test.go index 9b8ac32ed..0d6d4888a 100644 --- a/server/datastore/datastore_test.go +++ b/server/datastore/datastore_test.go @@ -18,6 +18,8 @@ func functionName(f func(*testing.T, kolide.Datastore)) string { var testFunctions = [...]func(*testing.T, kolide.Datastore){ testOrgInfo, testAdditionalQueries, + testEnrollSecrets, + testEnrollSecretRoundtrip, testCreateInvite, testInviteByEmail, testInviteByToken, diff --git a/server/datastore/inmem/hosts.go b/server/datastore/inmem/hosts.go index 189665582..50ab8e201 100644 --- a/server/datastore/inmem/hosts.go +++ b/server/datastore/inmem/hosts.go @@ -136,7 +136,7 @@ func (d *Datastore) GenerateHostStatusStatistics(now time.Time) (online, offline return online, offline, mia, new, nil } -func (d *Datastore) EnrollHost(osQueryHostID string, nodeKeySize int) (*kolide.Host, error) { +func (d *Datastore) EnrollHost(osQueryHostID, nodeKey, secretName string) (*kolide.Host, error) { d.mtx.Lock() defer d.mtx.Unlock() @@ -144,11 +144,6 @@ func (d *Datastore) EnrollHost(osQueryHostID string, nodeKeySize int) (*kolide.H return nil, errors.New("missing host identifier from osquery for host enrollment") } - nodeKey, err := kolide.RandomText(nodeKeySize) - if err != nil { - return nil, err - } - host := kolide.Host{ OsqueryHostID: osQueryHostID, NodeKey: nodeKey, diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go index 1960b0610..692d20adc 100644 --- a/server/datastore/mysql/app_configs.go +++ b/server/datastore/mysql/app_configs.go @@ -5,6 +5,7 @@ import ( "github.com/VividCortex/mysqlerr" "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" "github.com/kolide/fleet/server/kolide" "github.com/pkg/errors" ) @@ -94,7 +95,6 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error { org_name, org_logo_url, kolide_server_url, - osquery_enroll_secret, smtp_configured, smtp_sender_address, smtp_server, @@ -121,12 +121,11 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error { live_query_disabled, additional_queries ) - VALUES( 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) + VALUES( 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ON DUPLICATE KEY UPDATE org_name = VALUES(org_name), org_logo_url = VALUES(org_logo_url), kolide_server_url = VALUES(kolide_server_url), - osquery_enroll_secret = VALUES(osquery_enroll_secret), smtp_configured = VALUES(smtp_configured), smtp_sender_address = VALUES(smtp_sender_address), smtp_server = VALUES(smtp_server), @@ -158,7 +157,6 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error { info.OrgName, info.OrgLogoURL, info.KolideServerURL, - info.EnrollSecret, info.SMTPConfigured, info.SMTPSenderAddress, info.SMTPServer, @@ -188,3 +186,45 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error { return err } + +func (d *Datastore) VerifyEnrollSecret(secret string) (string, error) { + var s kolide.EnrollSecret + err := d.db.Get(&s, "SELECT name, active FROM enroll_secrets WHERE secret = ?", secret) + if err != nil { + return "", errors.New("no matching secret found") + } + if !s.Active { + return "", errors.New("secret is inactive") + } + + return s.Name, nil +} + +func (d *Datastore) ApplyEnrollSecretSpec(spec *kolide.EnrollSecretSpec) error { + err := d.withRetryTxx(func(tx *sqlx.Tx) error { + for _, secret := range spec.Secrets { + sql := ` + INSERT INTO enroll_secrets (name, secret, active) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE + secret = VALUES(secret), + active = VALUES(active) + ` + if _, err := tx.Exec(sql, secret.Name, secret.Secret, secret.Active); err != nil { + return errors.Wrap(err, "upsert secret") + } + } + return nil + }) + + return err +} + +func (d *Datastore) GetEnrollSecretSpec() (*kolide.EnrollSecretSpec, error) { + var spec kolide.EnrollSecretSpec + sql := `SELECT * FROM enroll_secrets` + if err := d.db.Select(&spec.Secrets, sql); err != nil { + return nil, errors.Wrap(err, "get secrets") + } + return &spec, nil +} diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 2868103f1..a3ee0ab6e 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -172,7 +172,8 @@ func (d *Datastore) SaveHost(host *kolide.Host) error { distributed_interval = ?, config_tls_refresh = ?, logger_tls_period = ?, - additional = COALESCE(?, additional) + additional = COALESCE(?, additional), + enroll_secret_name = ? WHERE id = ? ` err := d.withRetryTxx(func(tx *sqlx.Tx) error { @@ -205,6 +206,7 @@ func (d *Datastore) SaveHost(host *kolide.Host) error { host.ConfigTLSRefresh, host.LoggerTLSPeriod, host.Additional, + host.EnrollSecretName, host.ID, ) if err != nil { @@ -260,7 +262,6 @@ func (d *Datastore) DeleteHost(hid uint) error { return nil } -// TODO needs test func (d *Datastore) Host(id uint) (*kolide.Host, error) { sqlStatement := ` SELECT * FROM hosts @@ -442,24 +443,20 @@ func (d *Datastore) getNetInterfacesForHost(host *kolide.Host) error { } // EnrollHost enrolls a host -func (d *Datastore) EnrollHost(osqueryHostID string, nodeKeySize int) (*kolide.Host, error) { +func (d *Datastore) EnrollHost(osqueryHostID, nodeKey, secretName string) (*kolide.Host, error) { if osqueryHostID == "" { return nil, fmt.Errorf("missing osquery host identifier") } detailUpdateTime := time.Unix(0, 0).Add(24 * time.Hour) - nodeKey, err := kolide.RandomText(nodeKeySize) - if err != nil { - return nil, errors.Wrap(err, "generating random text") - } - sqlInsert := ` INSERT INTO hosts ( detail_update_time, osquery_host_id, seen_time, - node_key - ) VALUES (?, ?, ?, ?) + node_key, + enroll_secret_name + ) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE node_key = VALUES(node_key), deleted = FALSE @@ -467,7 +464,7 @@ func (d *Datastore) EnrollHost(osqueryHostID string, nodeKeySize int) (*kolide.H var result sql.Result - result, err = d.db.Exec(sqlInsert, detailUpdateTime, osqueryHostID, time.Now().UTC(), nodeKey) + result, err := d.db.Exec(sqlInsert, detailUpdateTime, osqueryHostID, time.Now().UTC(), nodeKey, secretName) if err != nil { return nil, errors.Wrap(err, "inserting") @@ -522,7 +519,8 @@ func (d *Datastore) AuthenticateHost(nodeKey string) (*kolide.Host, error) { seen_time, distributed_interval, logger_tls_period, - config_tls_refresh + config_tls_refresh, + enroll_secret_name FROM hosts WHERE node_key = ? AND NOT deleted LIMIT 1 diff --git a/server/datastore/mysql/migrations/tables/20200512120000_CreateTableEnrollSecrets.go b/server/datastore/mysql/migrations/tables/20200512120000_CreateTableEnrollSecrets.go new file mode 100644 index 000000000..aeb9fd2d2 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20200512120000_CreateTableEnrollSecrets.go @@ -0,0 +1,56 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20200512120000, Down_20200512120000) +} + +func Up_20200512120000(tx *sql.Tx) error { + _, err := tx.Exec( + "CREATE TABLE `enroll_secrets` (" + + "`name` VARCHAR(255) NOT NULL," + + "`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP," + + "`secret` VARCHAR(255) NOT NULL," + + "`active` TINYINT(1) DEFAULT TRUE," + + "PRIMARY KEY (`name`)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;", + ) + if err != nil { + return errors.Wrap(err, "create enroll_secrets table") + } + + _, err = tx.Exec( + "INSERT INTO `enroll_secrets` (`name`, `secret`, `active`)" + + "SELECT 'default', `osquery_enroll_secret`, TRUE FROM `app_configs`", + ) + if err != nil { + return errors.Wrap(err, "copy existing enroll secret") + } + + _, err = tx.Exec( + "ALTER TABLE `hosts`" + + "ADD COLUMN `enroll_secret_name` VARCHAR(255) NOT NULL DEFAULT ''", + ) + if err != nil { + return errors.Wrap(err, "drop old secret column") + } + + _, err = tx.Exec( + "ALTER TABLE `app_configs`" + + "DROP COLUMN `osquery_enroll_secret`", + ) + if err != nil { + return errors.Wrap(err, "drop old secret column") + } + + return nil +} + +func Down_20200512120000(tx *sql.Tx) error { + return nil +} diff --git a/server/kolide/app.go b/server/kolide/app.go index e23ef273c..f6014d4f5 100644 --- a/server/kolide/app.go +++ b/server/kolide/app.go @@ -3,6 +3,7 @@ package kolide import ( "context" "encoding/json" + "time" ) // AppConfigStore contains method for saving and retrieving @@ -11,6 +12,16 @@ type AppConfigStore interface { NewAppConfig(info *AppConfig) (*AppConfig, error) AppConfig() (*AppConfig, error) SaveAppConfig(info *AppConfig) error + + // VerifyEnrollSecret checks that the provided secret matches an active + // enroll secret. If it is successfully matched, the name of the secret is + // returned. Otherwise an error is returned. + VerifyEnrollSecret(secret string) (string, error) + // ApplyEnrollSecretSpec adds and updates the enroll secrets specified in + // the spec. + ApplyEnrollSecretSpec(spec *EnrollSecretSpec) error + // GetEnrollSecretSpec gets the spec for the current enroll secrets. + GetEnrollSecretSpec() (*EnrollSecretSpec, error) } // AppConfigService provides methods for configuring @@ -21,6 +32,12 @@ type AppConfigService interface { ModifyAppConfig(ctx context.Context, p AppConfigPayload) (info *AppConfig, err error) SendTestEmail(ctx context.Context, config *AppConfig) error + // ApplyEnrollSecretSpec adds and updates the enroll secrets specified in + // the spec. + ApplyEnrollSecretSpec(ctx context.Context, spec *EnrollSecretSpec) error + // GetEnrollSecretSpec gets the spec for the current enroll secrets. + GetEnrollSecretSpec(ctx context.Context) (*EnrollSecretSpec, error) + // Certificate returns the PEM encoded certificate chain for osqueryd TLS termination. // For cases where the connection is self-signed, the server will attempt to // connect using the InsecureSkipVerify option in tls.Config. @@ -84,11 +101,6 @@ type AppConfig struct { OrgLogoURL string `db:"org_logo_url"` KolideServerURL string `db:"kolide_server_url"` - // EnrollSecret is the config value that must be given by osqueryd hosts - // on enrollment. - // See https://osquery.readthedocs.io/en/stable/deployment/remote/#remote-authentication - EnrollSecret string `db:"osquery_enroll_secret"` - // SMTPConfigured is a flag that indicates if smtp has been successfully // tested with the settings provided by an admin user. SMTPConfigured bool `db:"smtp_configured"` @@ -238,7 +250,6 @@ type OrgInfo struct { // ServerSettings contains general settings about the kolide App. type ServerSettings struct { KolideServerURL *string `json:"kolide_server_url,omitempty"` - EnrollSecret *string `json:"osquery_enroll_secret,omitempty"` LiveQueryDisabled *bool `json:"live_query_disabled,omitempty"` } @@ -272,3 +283,23 @@ type ListOptions struct { // Direction of ordering OrderDirection OrderDirection } + +// EnrollSecret contains information about an enroll secret, name, and active +// status. Enroll secrets are used for osquery authentication. +type EnrollSecret struct { + // Name is the name assigned to the secret + Name string `json:"name" db:"name"` + // Secret is the actual secret key. + Secret string `json:"secret" db:"secret"` + // Active determines whether the secret is currently allowed to be used for + // authentication. + Active bool `json:"active" db:"active"` + // CreatedAt is the time this enroll secret was first added. + CreatedAt time.Time `json:"created_at" db:"created_at"` +} + +// EnrollSecretSpec is the fleetctl spec type for enroll secrets. +type EnrollSecretSpec struct { + // Secrets is the list of enroll secrets. + Secrets []EnrollSecret `json:"secrets"` +} diff --git a/server/kolide/hosts.go b/server/kolide/hosts.go index ba3498c59..65f2b0851 100644 --- a/server/kolide/hosts.go +++ b/server/kolide/hosts.go @@ -34,12 +34,14 @@ const ( ) type HostStore interface { + // NewHost is deprecated and will be removed. Hosts should always be + // enrolled via EnrollHost. NewHost(host *Host) (*Host, error) SaveHost(host *Host) error DeleteHost(hid uint) error Host(id uint) (*Host, error) ListHosts(opt ListOptions) ([]*Host, error) - EnrollHost(osqueryHostId string, nodeKeySize int) (*Host, error) + EnrollHost(osqueryHostId, nodeKey, secretName string) (*Host, error) // AuthenticateHost authenticates and returns host metadata by node key. // This method should not return the host "additional" information as this // is not typically necessary for the operations performed by the osquery @@ -113,6 +115,7 @@ type Host struct { ConfigTLSRefresh uint `json:"config_tls_refresh" db:"config_tls_refresh"` LoggerTLSPeriod uint `json:"logger_tls_period" db:"logger_tls_period"` Additional *json.RawMessage `json:"additional,omitempty" db:"additional"` + EnrollSecretName string `json:"enroll_secret_name" db:"enroll_secret_name"` } // HostSummary is a structure which represents a data summary about the total diff --git a/server/mock/datastore_appconfig.go b/server/mock/datastore_appconfig.go index 89fec941f..6764179dc 100644 --- a/server/mock/datastore_appconfig.go +++ b/server/mock/datastore_appconfig.go @@ -12,6 +12,12 @@ type AppConfigFunc func() (*kolide.AppConfig, error) type SaveAppConfigFunc func(info *kolide.AppConfig) error +type VerifyEnrollSecretFunc func(secret string) (string, error) + +type ApplyEnrollSecretSpecFunc func(spec *kolide.EnrollSecretSpec) error + +type GetEnrollSecretSpecFunc func() (*kolide.EnrollSecretSpec, error) + type AppConfigStore struct { NewAppConfigFunc NewAppConfigFunc NewAppConfigFuncInvoked bool @@ -21,6 +27,15 @@ type AppConfigStore struct { SaveAppConfigFunc SaveAppConfigFunc SaveAppConfigFuncInvoked bool + + VerifyEnrollSecretFunc VerifyEnrollSecretFunc + VerifyEnrollSecretFuncInvoked bool + + ApplyEnrollSecretSpecFunc ApplyEnrollSecretSpecFunc + ApplyEnrollSecretSpecFuncInvoked bool + + GetEnrollSecretSpecFunc GetEnrollSecretSpecFunc + GetEnrollSecretSpecFuncInvoked bool } func (s *AppConfigStore) NewAppConfig(info *kolide.AppConfig) (*kolide.AppConfig, error) { @@ -37,3 +52,18 @@ func (s *AppConfigStore) SaveAppConfig(info *kolide.AppConfig) error { s.SaveAppConfigFuncInvoked = true return s.SaveAppConfigFunc(info) } + +func (s *AppConfigStore) VerifyEnrollSecret(secret string) (string, error) { + s.VerifyEnrollSecretFuncInvoked = true + return s.VerifyEnrollSecretFunc(secret) +} + +func (s *AppConfigStore) ApplyEnrollSecretSpec(spec *kolide.EnrollSecretSpec) error { + s.ApplyEnrollSecretSpecFuncInvoked = true + return s.ApplyEnrollSecretSpecFunc(spec) +} + +func (s *AppConfigStore) GetEnrollSecretSpec() (*kolide.EnrollSecretSpec, error) { + s.GetEnrollSecretSpecFuncInvoked = true + return s.GetEnrollSecretSpecFunc() +} diff --git a/server/mock/datastore_hosts.go b/server/mock/datastore_hosts.go index e337bb49b..cb04a9b03 100644 --- a/server/mock/datastore_hosts.go +++ b/server/mock/datastore_hosts.go @@ -20,7 +20,7 @@ type HostFunc func(id uint) (*kolide.Host, error) type ListHostsFunc func(opt kolide.ListOptions) ([]*kolide.Host, error) -type EnrollHostFunc func(osqueryHostId string, nodeKeySize int) (*kolide.Host, error) +type EnrollHostFunc func(osqueryHostId, nodeKey, secretName string) (*kolide.Host, error) type AuthenticateHostFunc func(nodeKey string) (*kolide.Host, error) @@ -102,9 +102,9 @@ func (s *HostStore) ListHosts(opt kolide.ListOptions) ([]*kolide.Host, error) { return s.ListHostsFunc(opt) } -func (s *HostStore) EnrollHost(osqueryHostId string, nodeKeySize int) (*kolide.Host, error) { +func (s *HostStore) EnrollHost(osqueryHostId, nodeKey, secretName string) (*kolide.Host, error) { s.EnrollHostFuncInvoked = true - return s.EnrollHostFunc(osqueryHostId, nodeKeySize) + return s.EnrollHostFunc(osqueryHostId, nodeKey, secretName) } func (s *HostStore) AuthenticateHost(nodeKey string) (*kolide.Host, error) { diff --git a/server/service/client_appconfig.go b/server/service/client_appconfig.go index 38000eba9..858910c45 100644 --- a/server/service/client_appconfig.go +++ b/server/service/client_appconfig.go @@ -69,3 +69,61 @@ func (c *Client) GetServerSettings() (*kolide.ServerSettings, error) { } return appConfig.ServerSettings, nil } + +// GetEnrollSecretSpec fetches the enroll secrets stored on the server +func (c *Client) GetEnrollSecretSpec() (*kolide.EnrollSecretSpec, error) { + response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/enroll_secret", nil) + if err != nil { + return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/enroll_secret") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf( + "get enroll_secrets received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody getEnrollSecretSpecResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return nil, errors.Wrap(err, "decode get enroll secret spec response") + } + + if responseBody.Err != nil { + return nil, errors.Errorf("get enroll secret spec: %s", responseBody.Err) + } + + return responseBody.Spec, nil +} + +// ApplyEnrollSecretSpec applies the enroll secrets. +func (c *Client) ApplyEnrollSecretSpec(spec *kolide.EnrollSecretSpec) error { + req := applyEnrollSecretSpecRequest{Spec: spec} + response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/enroll_secret", req) + if err != nil { + return errors.Wrap(err, "POST /api/v1/kolide/spec/enroll_secret") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf( + "apply enroll secret received status %d %s", + response.StatusCode, + extractServerErrorText(response.Body), + ) + } + + var responseBody applyEnrollSecretSpecResponse + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + return errors.Wrap(err, "decode apply enroll secret response") + } + + if responseBody.Err != nil { + return errors.Errorf("apply enroll secret: %s", responseBody.Err) + } + return nil +} diff --git a/server/service/endpoint_appconfig.go b/server/service/endpoint_appconfig.go index 41b4366fa..84e56e857 100644 --- a/server/service/endpoint_appconfig.go +++ b/server/service/endpoint_appconfig.go @@ -65,7 +65,6 @@ func makeGetAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint { }, ServerSettings: &kolide.ServerSettings{ KolideServerURL: &config.KolideServerURL, - EnrollSecret: &config.EnrollSecret, LiveQueryDisabled: &config.LiveQueryDisabled, }, SMTPSettings: smtpSettings, @@ -93,7 +92,6 @@ func makeModifyAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint { }, ServerSettings: &kolide.ServerSettings{ KolideServerURL: &config.KolideServerURL, - EnrollSecret: &config.EnrollSecret, LiveQueryDisabled: &config.LiveQueryDisabled, }, SMTPSettings: smtpSettingsFromAppConfig(config), @@ -137,3 +135,49 @@ func smtpSettingsFromAppConfig(config *kolide.AppConfig) *kolide.SMTPSettingsPay SMTPEnableStartTLS: &config.SMTPEnableStartTLS, } } + +//////////////////////////////////////////////////////////////////////////////// +// Apply Enroll Secret Spec +//////////////////////////////////////////////////////////////////////////////// + +type applyEnrollSecretSpecRequest struct { + Spec *kolide.EnrollSecretSpec `json:"spec"` +} + +type applyEnrollSecretSpecResponse struct { + Err error `json:"error,omitempty"` +} + +func (r applyEnrollSecretSpecResponse) error() error { return r.Err } + +func makeApplyEnrollSecretSpecEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(applyEnrollSecretSpecRequest) + err := svc.ApplyEnrollSecretSpec(ctx, req.Spec) + if err != nil { + return applyEnrollSecretSpecResponse{Err: err}, nil + } + return applyEnrollSecretSpecResponse{}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Pack Specs +//////////////////////////////////////////////////////////////////////////////// + +type getEnrollSecretSpecResponse struct { + Spec *kolide.EnrollSecretSpec `json:"specs"` + Err error `json:"error,omitempty"` +} + +func (r getEnrollSecretSpecResponse) error() error { return r.Err } + +func makeGetEnrollSecretSpecEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + specs, err := svc.GetEnrollSecretSpec(ctx) + if err != nil { + return getEnrollSecretSpecResponse{Err: err}, nil + } + return getEnrollSecretSpecResponse{Spec: specs}, nil + } +} diff --git a/server/service/endpoint_middleware_test.go b/server/service/endpoint_middleware_test.go index 68a7737d3..4b2dc111f 100644 --- a/server/service/endpoint_middleware_test.go +++ b/server/service/endpoint_middleware_test.go @@ -3,12 +3,16 @@ package service import ( "context" "testing" + "time" "github.com/go-kit/kit/endpoint" "github.com/kolide/fleet/server/config" + hostctx "github.com/kolide/fleet/server/contexts/host" "github.com/kolide/fleet/server/contexts/viewer" "github.com/kolide/fleet/server/datastore/inmem" "github.com/kolide/fleet/server/kolide" + "github.com/kolide/fleet/server/mock" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -197,25 +201,36 @@ func TestGetNodeKey(t *testing.T) { } func TestAuthenticatedHost(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - require.Nil(t, err) - _, err = ds.NewAppConfig(&kolide.AppConfig{EnrollSecret: "foobarbaz"}) - require.Nil(t, err) + ds := new(mock.Store) svc, err := newTestService(ds, nil) require.Nil(t, err) + expectedHost := kolide.Host{HostName: "foo!"} + goodNodeKey := "foo bar baz bing bang boom" + + ds.AuthenticateHostFunc = func(secret string) (*kolide.Host, error) { + switch secret { + case goodNodeKey: + return &expectedHost, nil + default: + return nil, errors.New("no host found") + + } + } + ds.MarkHostSeenFunc = func(host *kolide.Host, t time.Time) error { + return nil + } + endpoint := authenticatedHost( svc, func(ctx context.Context, request interface{}) (interface{}, error) { + host, ok := hostctx.FromContext(ctx) + assert.True(t, ok) + assert.Equal(t, expectedHost, host) return nil, nil }, ) - ctx := context.Background() - goodNodeKey, err := svc.EnrollAgent(ctx, "foobarbaz", "host123", nil) - assert.Nil(t, err) - require.NotEmpty(t, goodNodeKey) - var authenticatedHostTests = []struct { nodeKey string shouldErr bool diff --git a/server/service/endpoint_setup.go b/server/service/endpoint_setup.go index 58104f34a..8bd0eabd7 100644 --- a/server/service/endpoint_setup.go +++ b/server/service/endpoint_setup.go @@ -42,9 +42,6 @@ func makeSetupEndpoint(svc kolide.Service) endpoint.Endpoint { if req.KolideServerURL != nil { configPayload.ServerSettings.KolideServerURL = req.KolideServerURL } - if req.EnrollSecret != nil { - configPayload.ServerSettings.EnrollSecret = req.EnrollSecret - } config, err = svc.NewAppConfig(ctx, configPayload) if err != nil { return setupResponse{Err: err}, nil @@ -81,7 +78,6 @@ func makeSetupEndpoint(svc kolide.Service) endpoint.Endpoint { OrgLogoURL: &config.OrgLogoURL, }, KolideServerURL: &config.KolideServerURL, - EnrollSecret: &config.EnrollSecret, Token: token, }, nil } diff --git a/server/service/handler.go b/server/service/handler.go index 3876475f0..a254e771c 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -36,6 +36,8 @@ type KolideEndpoints struct { DeleteSession endpoint.Endpoint GetAppConfig endpoint.Endpoint ModifyAppConfig endpoint.Endpoint + ApplyEnrollSecretSpec endpoint.Endpoint + GetEnrollSecretSpec endpoint.Endpoint CreateInvite endpoint.Endpoint ListInvites endpoint.Endpoint DeleteInvite endpoint.Endpoint @@ -138,6 +140,8 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string) Kol DeleteSession: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteSessionEndpoint(svc))), GetAppConfig: authenticatedUser(jwtKey, svc, canPerformActions(makeGetAppConfigEndpoint(svc))), ModifyAppConfig: authenticatedUser(jwtKey, svc, mustBeAdmin(makeModifyAppConfigEndpoint(svc))), + ApplyEnrollSecretSpec: authenticatedUser(jwtKey, svc, mustBeAdmin(makeApplyEnrollSecretSpecEndpoint(svc))), + GetEnrollSecretSpec: authenticatedUser(jwtKey, svc, canPerformActions(makeGetEnrollSecretSpecEndpoint(svc))), CreateInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeCreateInviteEndpoint(svc))), ListInvites: authenticatedUser(jwtKey, svc, mustBeAdmin(makeListInvitesEndpoint(svc))), DeleteInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteInviteEndpoint(svc))), @@ -225,6 +229,8 @@ type kolideHandlers struct { DeleteSession http.Handler GetAppConfig http.Handler ModifyAppConfig http.Handler + ApplyEnrollSecretSpec http.Handler + GetEnrollSecretSpec http.Handler CreateInvite http.Handler ListInvites http.Handler DeleteInvite http.Handler @@ -315,6 +321,8 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli DeleteSession: newServer(e.DeleteSession, decodeDeleteSessionRequest), GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest), ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest), + ApplyEnrollSecretSpec: newServer(e.ApplyEnrollSecretSpec, decodeApplyEnrollSecretSpecRequest), + GetEnrollSecretSpec: newServer(e.GetEnrollSecretSpec, decodeNoParamsRequest), CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest), ListInvites: newServer(e.ListInvites, decodeListInvitesRequest), DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest), @@ -446,6 +454,8 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) { r.Handle("/api/v1/kolide/config/certificate", h.GetCertificate).Methods("GET").Name("get_certificate") r.Handle("/api/v1/kolide/config", h.GetAppConfig).Methods("GET").Name("get_app_config") r.Handle("/api/v1/kolide/config", h.ModifyAppConfig).Methods("PATCH").Name("modify_app_config") + r.Handle("/api/v1/kolide/spec/enroll_secret", h.ApplyEnrollSecretSpec).Methods("POST").Name("apply_enroll_secret_spec") + r.Handle("/api/v1/kolide/spec/enroll_secret", h.GetEnrollSecretSpec).Methods("GET").Name("get_enroll_secret_spec") r.Handle("/api/v1/kolide/invites", h.CreateInvite).Methods("POST").Name("create_invite") r.Handle("/api/v1/kolide/invites", h.ListInvites).Methods("GET").Name("list_invites") r.Handle("/api/v1/kolide/invites/{id}", h.DeleteInvite).Methods("DELETE").Name("delete_invite") diff --git a/server/service/service_appconfig.go b/server/service/service_appconfig.go index 45da02793..1dc547fd0 100644 --- a/server/service/service_appconfig.go +++ b/server/service/service_appconfig.go @@ -36,18 +36,31 @@ func (svc service) NewAppConfig(ctx context.Context, p kolide.AppConfigPayload) return nil, err } fromPayload := appConfigFromAppConfigPayload(p, *config) - if fromPayload.EnrollSecret == "" { - // generate a random string if the user hasn't set one in the form. - rand, err := kolide.RandomText(24) - if err != nil { - return nil, errors.Wrap(err, "generate enroll secret string") - } - fromPayload.EnrollSecret = rand - } + newConfig, err := svc.ds.NewAppConfig(fromPayload) if err != nil { return nil, err } + + // Set up a default enroll secret + secret, err := kolide.RandomText(24) + if err != nil { + return nil, errors.Wrap(err, "generate enroll secret string") + } + spec := &kolide.EnrollSecretSpec{ + Secrets: []kolide.EnrollSecret{ + kolide.EnrollSecret{ + Name: "default", + Secret: secret, + Active: true, + }, + }, + } + err = svc.ds.ApplyEnrollSecretSpec(spec) + if err != nil { + return nil, errors.Wrap(err, "save enroll secret") + } + return newConfig, nil } @@ -117,9 +130,6 @@ func appConfigFromAppConfigPayload(p kolide.AppConfigPayload, config kolide.AppC if p.ServerSettings != nil && p.ServerSettings.KolideServerURL != nil { config.KolideServerURL = cleanupURL(*p.ServerSettings.KolideServerURL) } - if p.ServerSettings != nil && p.ServerSettings.EnrollSecret != nil { - config.EnrollSecret = *p.ServerSettings.EnrollSecret - } if p.ServerSettings != nil && p.ServerSettings.LiveQueryDisabled != nil { config.LiveQueryDisabled = *p.ServerSettings.LiveQueryDisabled } @@ -229,3 +239,11 @@ func appConfigFromAppConfigPayload(p kolide.AppConfigPayload, config kolide.AppC } return &config } + +func (svc service) ApplyEnrollSecretSpec(ctx context.Context, spec *kolide.EnrollSecretSpec) error { + return svc.ds.ApplyEnrollSecretSpec(spec) +} + +func (svc service) GetEnrollSecretSpec(ctx context.Context) (*kolide.EnrollSecretSpec, error) { + return svc.ds.GetEnrollSecretSpec() +} diff --git a/server/service/service_appconfig_test.go b/server/service/service_appconfig_test.go index 8ec9d7860..752a0cd56 100644 --- a/server/service/service_appconfig_test.go +++ b/server/service/service_appconfig_test.go @@ -4,9 +4,8 @@ import ( "context" "testing" - "github.com/kolide/fleet/server/config" - "github.com/kolide/fleet/server/datastore/inmem" "github.com/kolide/fleet/server/kolide" + "github.com/kolide/fleet/server/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -32,12 +31,14 @@ func TestCleanupURL(t *testing.T) { } func TestCreateAppConfig(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - require.Nil(t, err) - require.Nil(t, ds.MigrateData()) - + ds := new(mock.Store) svc, err := newTestService(ds, nil) require.Nil(t, err) + + ds.AppConfigFunc = func() (*kolide.AppConfig, error) { + return &kolide.AppConfig{}, nil + } + var appConfigTests = []struct { configPayload kolide.AppConfigPayload }{ @@ -48,7 +49,7 @@ func TestCreateAppConfig(t *testing.T) { OrgName: stringPtr("Acme"), }, ServerSettings: &kolide.ServerSettings{ - KolideServerURL: stringPtr("https://acme.co:8080/"), + KolideServerURL: stringPtr("https://acme.co:8080/"), LiveQueryDisabled: boolPtr(true), }, }, @@ -56,14 +57,30 @@ func TestCreateAppConfig(t *testing.T) { } for _, tt := range appConfigTests { - result, err := svc.NewAppConfig(context.Background(), tt.configPayload) + var result *kolide.AppConfig + ds.NewAppConfigFunc = func(config *kolide.AppConfig) (*kolide.AppConfig, error) { + result = config + return config, nil + } + + var gotSecretSpec *kolide.EnrollSecretSpec + ds.ApplyEnrollSecretSpecFunc = func(spec *kolide.EnrollSecretSpec) error { + gotSecretSpec = spec + return nil + } + + _, err := svc.NewAppConfig(context.Background(), tt.configPayload) require.Nil(t, err) payload := tt.configPayload - assert.NotEmpty(t, result.ID) assert.Equal(t, *payload.OrgInfo.OrgLogoURL, result.OrgLogoURL) assert.Equal(t, *payload.OrgInfo.OrgName, result.OrgName) assert.Equal(t, "https://acme.co:8080", result.KolideServerURL) assert.Equal(t, *payload.ServerSettings.LiveQueryDisabled, result.LiveQueryDisabled) + + // Ensure enroll secret was set + require.NotNil(t, gotSecretSpec) + require.Len(t, gotSecretSpec.Secrets, 1) + assert.Len(t, gotSecretSpec.Secrets[0].Secret, 32) } } diff --git a/server/service/service_osquery.go b/server/service/service_osquery.go index 57067e5e4..7cba905e3 100644 --- a/server/service/service_osquery.go +++ b/server/service/service_osquery.go @@ -72,18 +72,25 @@ func (svc service) AuthenticateHost(ctx context.Context, nodeKey string) (*kolid } func (svc service) EnrollAgent(ctx context.Context, enrollSecret, hostIdentifier string, hostDetails map[string](map[string]string)) (string, error) { - config, err := svc.ds.AppConfig() + secretName, err := svc.ds.VerifyEnrollSecret(enrollSecret) if err != nil { - return "", osqueryError{message: "getting enroll secret: " + err.Error(), nodeInvalid: true} + return "", osqueryError{ + message: "enroll failed: " + err.Error(), + nodeInvalid: true, + } } - if enrollSecret != config.EnrollSecret { - return "", osqueryError{message: "invalid enroll secret", nodeInvalid: true} + nodeKey, err := kolide.RandomText(svc.config.Osquery.NodeKeySize) + if err != nil { + return "", osqueryError{ + message: "generate node key failed: " + err.Error(), + nodeInvalid: true, + } } - host, err := svc.ds.EnrollHost(hostIdentifier, svc.config.Osquery.NodeKeySize) + host, err := svc.ds.EnrollHost(hostIdentifier, nodeKey, secretName) if err != nil { - return "", osqueryError{message: "enrollment failed: " + err.Error(), nodeInvalid: true} + return "", osqueryError{message: "save enroll failed: " + err.Error(), nodeInvalid: true} } // Save enrollment details if provided diff --git a/server/service/service_osquery_test.go b/server/service/service_osquery_test.go index 389a01d39..9df00be7b 100644 --- a/server/service/service_osquery_test.go +++ b/server/service/service_osquery_test.go @@ -24,42 +24,66 @@ import ( ) func TestEnrollAgent(t *testing.T) { - ds, svc, _ := setupOsqueryTests(t) - ctx := context.Background() + ds := new(mock.Store) + ds.VerifyEnrollSecretFunc = func(secret string) (string, error) { + switch secret { + case "valid_secret": + return "valid", nil + default: + return "", errors.New("not found") + } + } + ds.EnrollHostFunc = func(osqueryHostId, nodeKey, secretName string) (*kolide.Host, error) { + return &kolide.Host{ + OsqueryHostID: osqueryHostId, NodeKey: nodeKey, EnrollSecretName: secretName, + }, nil + } - hosts, err := ds.ListHosts(kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, hosts, 0) + svc, err := newTestService(ds, nil) + require.Nil(t, err) - nodeKey, err := svc.EnrollAgent(ctx, "", "host123", nil) + nodeKey, err := svc.EnrollAgent(context.Background(), "valid_secret", "host123", nil) require.Nil(t, err) assert.NotEmpty(t, nodeKey) - - hosts, err = ds.ListHosts(kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, hosts, 1) } func TestEnrollAgentIncorrectEnrollSecret(t *testing.T) { - ds, svc, _ := setupOsqueryTests(t) - ctx := context.Background() + ds := new(mock.Store) + ds.VerifyEnrollSecretFunc = func(secret string) (string, error) { + switch secret { + case "valid_secret": + return "valid", nil + default: + return "", errors.New("not found") + } + } - hosts, err := ds.ListHosts(kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, hosts, 0) + svc, err := newTestService(ds, nil) + require.Nil(t, err) - nodeKey, err := svc.EnrollAgent(ctx, "not_correct", "host123", nil) + nodeKey, err := svc.EnrollAgent(context.Background(), "not_correct", "host123", nil) assert.NotNil(t, err) assert.Empty(t, nodeKey) - - hosts, err = ds.ListHosts(kolide.ListOptions{}) - assert.Nil(t, err) - assert.Len(t, hosts, 0) } func TestEnrollAgentDetails(t *testing.T) { - ds, svc, _ := setupOsqueryTests(t) - ctx := context.Background() + ds := new(mock.Store) + ds.VerifyEnrollSecretFunc = func(secret string) (string, error) { + return "valid", nil + } + ds.EnrollHostFunc = func(osqueryHostId, nodeKey, secretName string) (*kolide.Host, error) { + return &kolide.Host{ + OsqueryHostID: osqueryHostId, NodeKey: nodeKey, EnrollSecretName: secretName, + }, nil + } + var gotHost *kolide.Host + ds.SaveHostFunc = func(host *kolide.Host) error { + gotHost = host + return nil + } + + svc, err := newTestService(ds, nil) + require.Nil(t, err) details := map[string](map[string]string){ "osquery_info": {"version": "2.12.0"}, @@ -73,48 +97,51 @@ func TestEnrollAgentDetails(t *testing.T) { }, "foo": {"foo": "bar"}, } - nodeKey, err := svc.EnrollAgent(ctx, "", "host123", details) + nodeKey, err := svc.EnrollAgent(context.Background(), "", "host123", details) require.Nil(t, err) assert.NotEmpty(t, nodeKey) - hosts, err := ds.ListHosts(kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, hosts, 1) - - h := hosts[0] - assert.Equal(t, "Mac OS X 10.14.5", h.OSVersion) - assert.Equal(t, "darwin", h.Platform) - assert.Equal(t, "2.12.0", h.OsqueryVersion) - assert.Equal(t, "zwass.local", h.HostName) - assert.Equal(t, "froobling_uuid", h.UUID) + assert.Equal(t, "Mac OS X 10.14.5", gotHost.OSVersion) + assert.Equal(t, "darwin", gotHost.Platform) + assert.Equal(t, "2.12.0", gotHost.OsqueryVersion) + assert.Equal(t, "zwass.local", gotHost.HostName) + assert.Equal(t, "froobling_uuid", gotHost.UUID) + assert.Equal(t, "valid", gotHost.EnrollSecretName) } func TestAuthenticateHost(t *testing.T) { - ds, svc, mockClock := setupOsqueryTests(t) - ctx := context.Background() - - nodeKey, err := svc.EnrollAgent(ctx, "", "host123", nil) + ds := new(mock.Store) + svc, err := newTestService(ds, nil) require.Nil(t, err) - mockClock.AddTime(1 * time.Minute) + var gotKey string + host := kolide.Host{HostName: "foobar"} + ds.AuthenticateHostFunc = func(key string) (*kolide.Host, error) { + gotKey = key + return &host, nil + } + ds.MarkHostSeenFunc = func(host *kolide.Host, t time.Time) error { + return nil + } - host, err := svc.AuthenticateHost(ctx, nodeKey) + h, err := svc.AuthenticateHost(context.Background(), "test") + require.Nil(t, err) + assert.Equal(t, "test", gotKey) + assert.True(t, ds.MarkHostSeenFuncInvoked) + assert.Equal(t, host, *h) +} + +func TestAuthenticateHostFailure(t *testing.T) { + ds := new(mock.Store) + svc, err := newTestService(ds, nil) require.Nil(t, err) - // Verify that the update time is set appropriately - checkHost, err := ds.Host(host.ID) - require.Nil(t, err) - assert.Equal(t, mockClock.Now(), checkHost.UpdatedAt) + ds.AuthenticateHostFunc = func(key string) (*kolide.Host, error) { + return nil, errors.New("not found") + } - // Advance clock time and check that seen time is updated - mockClock.AddTime(1*time.Minute + 27*time.Second) - - _, err = svc.AuthenticateHost(ctx, nodeKey) - require.Nil(t, err) - - checkHost, err = ds.Host(host.ID) - require.Nil(t, err) - assert.Equal(t, mockClock.Now(), checkHost.UpdatedAt) + _, err = svc.AuthenticateHost(context.Background(), "test") + require.NotNil(t, err) } type testJSONLogger struct { @@ -127,18 +154,10 @@ func (n *testJSONLogger) Write(ctx context.Context, logs []json.RawMessage) erro } func TestSubmitStatusLogs(t *testing.T) { - ds, svc, _ := setupOsqueryTests(t) - ctx := context.Background() - - _, err := svc.EnrollAgent(ctx, "", "host123", nil) + ds := new(mock.Store) + svc, err := newTestService(ds, nil) require.Nil(t, err) - hosts, err := ds.ListHosts(kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, hosts, 1) - host := hosts[0] - ctx = hostctx.NewContext(ctx, *host) - // Hack to get at the service internals and modify the writer serv := ((svc.(validationMiddleware)).Service).(service) @@ -155,6 +174,8 @@ func TestSubmitStatusLogs(t *testing.T) { err = json.Unmarshal([]byte(logJSON), &status) require.Nil(t, err) + host := kolide.Host{} + ctx := hostctx.NewContext(context.Background(), host) err = serv.SubmitStatusLogs(ctx, status) assert.Nil(t, err) @@ -162,18 +183,10 @@ func TestSubmitStatusLogs(t *testing.T) { } func TestSubmitResultLogs(t *testing.T) { - ds, svc, _ := setupOsqueryTests(t) - ctx := context.Background() - - _, err := svc.EnrollAgent(ctx, "", "host123", nil) + ds := new(mock.Store) + svc, err := newTestService(ds, nil) require.Nil(t, err) - hosts, err := ds.ListHosts(kolide.ListOptions{}) - require.Nil(t, err) - require.Len(t, hosts, 1) - host := hosts[0] - ctx = hostctx.NewContext(ctx, *host) - // Hack to get at the service internals and modify the writer serv := ((svc.(validationMiddleware)).Service).(service) @@ -194,6 +207,8 @@ func TestSubmitResultLogs(t *testing.T) { err = json.Unmarshal([]byte(logJSON), &results) require.Nil(t, err) + host := kolide.Host{} + ctx := hostctx.NewContext(context.Background(), host) err = serv.SubmitResultLogs(ctx, results) assert.Nil(t, err) @@ -496,16 +511,23 @@ func TestGetClientConfig(t *testing.T) { } func TestDetailQueriesWithEmptyStrings(t *testing.T) { - ds, svc, mockClock := setupOsqueryTests(t) - ctx := context.Background() - - nodeKey, err := svc.EnrollAgent(ctx, "", "host123", nil) - assert.Nil(t, err) - - host, err := ds.AuthenticateHost(nodeKey) + ds := new(mock.Store) + mockClock := clock.NewMockClock() + svc, err := newTestServiceWithClock(ds, nil, mockClock) require.Nil(t, err) - ctx = hostctx.NewContext(ctx, *host) + host := kolide.Host{} + ctx := hostctx.NewContext(context.Background(), host) + + ds.AppConfigFunc = func() (*kolide.AppConfig, error) { + return &kolide.AppConfig{}, nil + } + ds.LabelQueriesForHostFunc = func(*kolide.Host, time.Time) (map[string]string, error) { + return map[string]string{}, nil + } + ds.DistributedQueriesForHostFunc = func(*kolide.Host) (map[uint]string, error) { + return map[uint]string{}, nil + } // With a new host, we should get the detail queries (and accelerated // queries) @@ -606,39 +628,41 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) { err = json.Unmarshal([]byte(resultJSON), &results) require.Nil(t, err) + var gotHost *kolide.Host + ds.SaveHostFunc = func(host *kolide.Host) error { + gotHost = host + return nil + } + // Verify that results are ingested properly svc.SubmitDistributedQueryResults(ctx, results, map[string]kolide.OsqueryStatus{}) - // Make sure the result saved to the datastore - host, err = ds.AuthenticateHost(nodeKey) - require.Nil(t, err) - // osquery_info - assert.Equal(t, "darwin", host.Platform) - assert.Equal(t, "1.8.2", host.OsqueryVersion) + assert.Equal(t, "darwin", gotHost.Platform) + assert.Equal(t, "1.8.2", gotHost.OsqueryVersion) // system_info - assert.Equal(t, 17179869184, host.PhysicalMemory) - assert.Equal(t, "computer.local", host.HostName) - assert.Equal(t, "uuid", host.UUID) + assert.Equal(t, 17179869184, gotHost.PhysicalMemory) + assert.Equal(t, "computer.local", gotHost.HostName) + assert.Equal(t, "uuid", gotHost.UUID) // os_version - assert.Equal(t, "Mac OS X 10.10.6", host.OSVersion) + assert.Equal(t, "Mac OS X 10.10.6", gotHost.OSVersion) // uptime - assert.Equal(t, 1730893*time.Second, host.Uptime) + assert.Equal(t, 1730893*time.Second, gotHost.Uptime) // osquery_flags - assert.Equal(t, uint(0), host.ConfigTLSRefresh) - assert.Equal(t, uint(0), host.DistributedInterval) - assert.Equal(t, uint(0), host.LoggerTLSPeriod) + assert.Equal(t, uint(0), gotHost.ConfigTLSRefresh) + assert.Equal(t, uint(0), gotHost.DistributedInterval) + assert.Equal(t, uint(0), gotHost.LoggerTLSPeriod) + host.HostName = "computer.local" + host.DetailUpdateTime = mockClock.Now() mockClock.AddTime(1 * time.Minute) // Now no detail queries should be required - host, err = ds.AuthenticateHost(nodeKey) - require.Nil(t, err) - ctx = hostctx.NewContext(ctx, *host) + ctx = hostctx.NewContext(context.Background(), host) queries, acc, err = svc.GetDistributedQueries(ctx) assert.Nil(t, err) assert.Len(t, queries, 0) @@ -647,12 +671,6 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) { // Advance clock and queries should exist again mockClock.AddTime(1*time.Hour + 1*time.Minute) - err = svc.SubmitDistributedQueryResults(ctx, kolide.OsqueryDistributedQueryResults{}, map[string]kolide.OsqueryStatus{}) - require.Nil(t, err) - host, err = ds.AuthenticateHost(nodeKey) - require.Nil(t, err) - - ctx = hostctx.NewContext(ctx, *host) queries, acc, err = svc.GetDistributedQueries(ctx) assert.Nil(t, err) assert.Len(t, queries, len(detailQueries)) @@ -660,16 +678,23 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) { } func TestDetailQueries(t *testing.T) { - ds, svc, mockClock := setupOsqueryTests(t) - ctx := context.Background() - - nodeKey, err := svc.EnrollAgent(ctx, "", "host123", nil) - assert.Nil(t, err) - - host, err := ds.AuthenticateHost(nodeKey) + ds := new(mock.Store) + mockClock := clock.NewMockClock() + svc, err := newTestServiceWithClock(ds, nil, mockClock) require.Nil(t, err) - ctx = hostctx.NewContext(ctx, *host) + host := kolide.Host{} + ctx := hostctx.NewContext(context.Background(), host) + + ds.AppConfigFunc = func() (*kolide.AppConfig, error) { + return &kolide.AppConfig{}, nil + } + ds.LabelQueriesForHostFunc = func(*kolide.Host, time.Time) (map[string]string, error) { + return map[string]string{}, nil + } + ds.DistributedQueriesForHostFunc = func(*kolide.Host) (map[uint]string, error) { + return map[uint]string{}, nil + } // With a new host, we should get the detail queries (and accelerated // queries) @@ -774,39 +799,40 @@ func TestDetailQueries(t *testing.T) { err = json.Unmarshal([]byte(resultJSON), &results) require.Nil(t, err) + var gotHost *kolide.Host + ds.SaveHostFunc = func(host *kolide.Host) error { + gotHost = host + return nil + } // Verify that results are ingested properly svc.SubmitDistributedQueryResults(ctx, results, map[string]kolide.OsqueryStatus{}) - // Make sure the result saved to the datastore - host, err = ds.AuthenticateHost(nodeKey) - require.Nil(t, err) - // osquery_info - assert.Equal(t, "darwin", host.Platform) - assert.Equal(t, "1.8.2", host.OsqueryVersion) + assert.Equal(t, "darwin", gotHost.Platform) + assert.Equal(t, "1.8.2", gotHost.OsqueryVersion) // system_info - assert.Equal(t, 17179869184, host.PhysicalMemory) - assert.Equal(t, "computer.local", host.HostName) - assert.Equal(t, "uuid", host.UUID) + assert.Equal(t, 17179869184, gotHost.PhysicalMemory) + assert.Equal(t, "computer.local", gotHost.HostName) + assert.Equal(t, "uuid", gotHost.UUID) // os_version - assert.Equal(t, "Mac OS X 10.10.6", host.OSVersion) + assert.Equal(t, "Mac OS X 10.10.6", gotHost.OSVersion) // uptime - assert.Equal(t, 1730893*time.Second, host.Uptime) + assert.Equal(t, 1730893*time.Second, gotHost.Uptime) // osquery_flags - assert.Equal(t, uint(10), host.ConfigTLSRefresh) - assert.Equal(t, uint(5), host.DistributedInterval) - assert.Equal(t, uint(60), host.LoggerTLSPeriod) + assert.Equal(t, uint(10), gotHost.ConfigTLSRefresh) + assert.Equal(t, uint(5), gotHost.DistributedInterval) + assert.Equal(t, uint(60), gotHost.LoggerTLSPeriod) + host.HostName = "computer.local" + host.DetailUpdateTime = mockClock.Now() mockClock.AddTime(1 * time.Minute) // Now no detail queries should be required - host, err = ds.AuthenticateHost(nodeKey) - require.Nil(t, err) - ctx = hostctx.NewContext(ctx, *host) + ctx = hostctx.NewContext(ctx, host) queries, acc, err = svc.GetDistributedQueries(ctx) assert.Nil(t, err) assert.Len(t, queries, 0) @@ -815,12 +841,6 @@ func TestDetailQueries(t *testing.T) { // Advance clock and queries should exist again mockClock.AddTime(1*time.Hour + 1*time.Minute) - err = svc.SubmitDistributedQueryResults(ctx, kolide.OsqueryDistributedQueryResults{}, map[string]kolide.OsqueryStatus{}) - require.Nil(t, err) - host, err = ds.AuthenticateHost(nodeKey) - require.Nil(t, err) - - ctx = hostctx.NewContext(ctx, *host) queries, acc, err = svc.GetDistributedQueries(ctx) assert.Nil(t, err) assert.Len(t, queries, len(detailQueries)) @@ -995,63 +1015,49 @@ func TestDistributedQueryResults(t *testing.T) { } func TestOrphanedQueryCampaign(t *testing.T) { - ds, err := inmem.New(config.TestConfig()) - require.Nil(t, err) - - _, err = ds.NewAppConfig(&kolide.AppConfig{EnrollSecret: ""}) - require.Nil(t, err) - + ds := new(mock.Store) rs := pubsub.NewInmemQueryResults() svc, err := newTestService(ds, rs) require.Nil(t, err) - ctx := context.Background() + ds.DistributedQueryCampaignFunc = func(id uint) (*kolide.DistributedQueryCampaign, error) { + return &kolide.DistributedQueryCampaign{ID: 1}, nil + } + ds.NewDistributedQueryExecutionFunc = func(*kolide.DistributedQueryExecution) (*kolide.DistributedQueryExecution, error) { + return nil, nil + } - nodeKey, err := svc.EnrollAgent(ctx, "", "host123", nil) - require.Nil(t, err) - - host, err := ds.AuthenticateHost(nodeKey) - require.Nil(t, err) - - ctx = viewer.NewContext(context.Background(), viewer.Viewer{ - User: &kolide.User{ - ID: 0, - }, - }) - q := "select year, month, day, hour, minutes, seconds from time" - campaign, err := svc.NewDistributedQueryCampaign(ctx, q, []uint{}, []uint{}) - require.Nil(t, err) - - campaign.Status = kolide.QueryRunning - err = ds.SaveDistributedQueryCampaign(campaign) - require.Nil(t, err) - - queryKey := fmt.Sprintf("%s%d", hostDistributedQueryPrefix, campaign.ID) + savedCampaign := &kolide.DistributedQueryCampaign{} + ds.SaveDistributedQueryCampaignFunc = func(campaign *kolide.DistributedQueryCampaign) error { + savedCampaign = campaign + return nil + } + // Submit results + queryKey := hostDistributedQueryPrefix + "1" expectedRows := []map[string]string{ - { - "year": "2016", - "month": "11", - "day": "11", - "hour": "6", - "minutes": "12", - "seconds": "10", + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "baz": "boom", }, } + host := kolide.Host{HostName: "the fooer"} results := map[string][]map[string]string{ queryKey: expectedRows, } - // Submit results - ctx = hostctx.NewContext(context.Background(), *host) + ctx := context.Background() + ctx = hostctx.NewContext(context.Background(), host) err = svc.SubmitDistributedQueryResults(ctx, results, map[string]kolide.OsqueryStatus{}) require.Nil(t, err) - // The campaign should be set to completed because it is orphaned - campaign, err = ds.DistributedQueryCampaign(campaign.ID) - require.Nil(t, err) - assert.Equal(t, kolide.QueryComplete, campaign.Status) + // Ensure that status is changed to completed when there is no listener for + // results. + require.NotNil(t, savedCampaign) + assert.Equal(t, kolide.QueryComplete, savedCampaign.Status) } func TestUpdateHostIntervals(t *testing.T) { @@ -1184,9 +1190,6 @@ func setupOsqueryTests(t *testing.T) (kolide.Datastore, kolide.Service, *clock.M ds, err := inmem.New(config.TestConfig()) require.Nil(t, err) - _, err = ds.NewAppConfig(&kolide.AppConfig{EnrollSecret: ""}) - require.Nil(t, err) - mockClock := clock.NewMockClock() svc, err := newTestServiceWithClock(ds, nil, mockClock) require.Nil(t, err) diff --git a/server/service/transport_appconfig.go b/server/service/transport_appconfig.go index cd5b77c3f..2ef2ed401 100644 --- a/server/service/transport_appconfig.go +++ b/server/service/transport_appconfig.go @@ -15,3 +15,12 @@ func decodeModifyAppConfigRequest(ctx context.Context, r *http.Request) (interfa } return appConfigRequest{Payload: payload}, nil } + +func decodeApplyEnrollSecretSpecRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req applyEnrollSecretSpecRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil + +}