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
This commit is contained in:
Zachary Wasserman 2020-05-29 09:12:39 -07:00 committed by GitHub
parent 619e36755c
commit c1aa8355cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1218 additions and 429 deletions

View File

@ -24,6 +24,7 @@ type specGroup struct {
Labels []*kolide.LabelSpec Labels []*kolide.LabelSpec
Options *kolide.OptionsSpec Options *kolide.OptionsSpec
AppConfig *kolide.AppConfigPayload AppConfig *kolide.AppConfigPayload
EnrollSecret *kolide.EnrollSecretSpec
} }
func specGroupFromBytes(b []byte) (*specGroup, error) { func specGroupFromBytes(b []byte) (*specGroup, error) {
@ -91,6 +92,17 @@ func specGroupFromBytes(b []byte) (*specGroup, error) {
} }
specs.AppConfig = appConfigSpec 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: default:
return nil, errors.Errorf("unknown kind %q", s.Kind) 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 return nil
}, },
} }

View File

@ -392,8 +392,9 @@ func getOptionsCommand() cli.Command {
func getEnrollSecretCommand() cli.Command { func getEnrollSecretCommand() cli.Command {
return cli.Command{ return cli.Command{
Name: "enroll-secret", Name: "enroll_secret",
Usage: "Retrieve the osquery enroll secret", Aliases: []string{"enroll_secrets", "enroll-secret", "enroll-secrets"},
Usage: "Retrieve the osquery enroll secrets",
Flags: []cli.Flag{ Flags: []cli.Flag{
configFlag(), configFlag(),
contextFlag(), contextFlag(),
@ -404,16 +405,23 @@ func getEnrollSecretCommand() cli.Command {
return err return err
} }
settings, err := fleet.GetServerSettings() secrets, err := fleet.GetEnrollSecretSpec()
if err != nil { if err != nil {
return err 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 return nil
}, },
} }

View File

@ -255,7 +255,6 @@ spec:
org_name: Example Org org_name: Example Org
server_settings: server_settings:
kolide_server_url: https://fleet.example.org:8080 kolide_server_url: https://fleet.example.org:8080
osquery_enroll_secret: supersekretsecret
smtp_settings: smtp_settings:
authentication_method: authmethod_plain authentication_method: authmethod_plain
authentication_type: authtype_username_password authentication_type: authtype_username_password
@ -291,3 +290,25 @@ The following options are available when configuring SMTP authentication:
- `authmethod_cram_md5` - `authmethod_cram_md5`
- `authmethod_login` - `authmethod_login`
- `authmethod_plain` - `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!
```

View File

@ -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 - `--hostname`: the hostname of the gRPC server for your environment
- `--root_directory`: the location of the local database, pidfiles, etc. - `--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 \ ./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: 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 - `--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: 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 #### 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`) - 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`) - 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. 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. 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. 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 #### 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. 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. 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)

View File

@ -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!

View File

@ -69,6 +69,20 @@ spec:
3600: "SELECT total_seconds AS uptime FROM uptime" 3600: "SELECT total_seconds AS uptime FROM uptime"
--- ---
apiVersion: v1 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 kind: label
spec: spec:
name: pending_updates name: pending_updates

View File

@ -6,7 +6,7 @@ import classnames from 'classnames';
import { authToken } from 'utilities/local'; import { authToken } from 'utilities/local';
import { fetchCurrentUser } from 'redux/nodes/auth/actions'; 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'; import userInterface from 'interfaces/user';
export class App extends Component { export class App extends Component {
@ -32,6 +32,8 @@ export class App extends Component {
if (user) { if (user) {
dispatch(getConfig()) dispatch(getConfig())
.catch(() => false); .catch(() => false);
dispatch(getEnrollSecret())
.catch(() => false);
} }
return false; return false;
@ -43,6 +45,8 @@ export class App extends Component {
if (user && this.props.user !== user) { if (user && this.props.user !== user) {
dispatch(getConfig()) dispatch(getConfig())
.catch(() => false); .catch(() => false);
dispatch(getEnrollSecret())
.catch(() => false);
} }
} }

View File

@ -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 (
<span>
{name}
<span className="buttons">
{copyMessage && <span>{`${copyMessage} `}</span>}
<Button
variant="unstyled"
className={`${baseClass}__secret-copy-icon`}
onClick={onCopySecret}
>
<Icon name="clipboard" />
</Button>
<a
href="#showSecret"
onClick={onToggleSecret}
className={`${baseClass}__show-secret`}
>
{showSecret ? 'Hide' : 'Show'}
</a>
</span>
</span>
);
}
render () {
const { secret } = this.props;
const { showSecret } = this.state;
const { renderLabel } = this;
return (
<div>
<InputField
disabled
inputWrapperClass={`${baseClass}__secret-input`}
name="osqueryd-secret"
label={renderLabel()}
type={showSecret ? 'text' : 'password'}
value={secret}
/>
</div>
);
}
}
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 (<div className={baseClass}><em>No active enroll secrets.</em></div>);
}
return (
<div className={baseClass}>
{activeSecrets.map(({ name, secret }) =>
<EnrollSecretRow key={name} name={name} secret={secret} />
)}
</div>
);
}
}
export default EnrollSecretTable;
export { EnrollSecretRow };

View File

@ -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(<EnrollSecretTable {...defaultProps} />);
expect(table.find('EnrollSecretRow').length).toEqual(2);
});
it('renders text when empty', () => {
const table = shallow(<EnrollSecretTable secrets={[]} />);
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(<EnrollSecretRow {...defaultProps} />);
const inputField = row.find('InputField').find('input');
expect(inputField.prop('type')).toEqual('password');
});
it('should show secret when enabled', () => {
const row = mount(<EnrollSecretRow {...defaultProps} />);
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(<EnrollSecretRow {...defaultProps} />);
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(<EnrollSecretRow {...defaultProps} />);
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);
});
});

View File

@ -0,0 +1,9 @@
.enroll-secrets {
max-height: 10em;
overflow: auto;
.buttons {
float: right;
}
}

View File

@ -0,0 +1 @@
export default from './EnrollSecretTable';

View File

@ -10,7 +10,7 @@ class FormField extends Component {
className: PropTypes.string, className: PropTypes.string,
error: PropTypes.string, error: PropTypes.string,
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.node, 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, name: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
}; };

View File

@ -6,6 +6,8 @@ import Checkbox from 'components/forms/fields/Checkbox';
import Dropdown from 'components/forms/fields/Dropdown'; import Dropdown from 'components/forms/fields/Dropdown';
import Form from 'components/forms/Form'; import Form from 'components/forms/Form';
import formFieldInterface from 'interfaces/form_field'; 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 Icon from 'components/icons/Icon';
import InputField from 'components/forms/fields/InputField'; import InputField from 'components/forms/fields/InputField';
import OrgLogoIcon from 'components/icons/OrgLogoIcon'; import OrgLogoIcon from 'components/icons/OrgLogoIcon';
@ -66,6 +68,7 @@ class AppConfigForm extends Component {
host_expiry_window: formFieldInterface.isRequired, host_expiry_window: formFieldInterface.isRequired,
live_query_disabled: formFieldInterface.isRequired, live_query_disabled: formFieldInterface.isRequired,
}).isRequired, }).isRequired,
enrollSecret: enrollSecretInterface.isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
smtpConfigured: PropTypes.bool.isRequired, smtpConfigured: PropTypes.bool.isRequired,
}; };
@ -73,7 +76,7 @@ class AppConfigForm extends Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.state = { revealSecret: false, showAdvancedOptions: false }; this.state = { showAdvancedOptions: false };
} }
onToggleAdvancedOptions = (evt) => { onToggleAdvancedOptions = (evt) => {
@ -86,16 +89,6 @@ class AppConfigForm extends Component {
return false; return false;
} }
onToggleRevealSecret = (evt) => {
evt.preventDefault();
const { revealSecret } = this.state;
this.setState({ revealSecret: !revealSecret });
return false;
}
renderAdvancedOptions = () => { renderAdvancedOptions = () => {
const { fields } = this.props; const { fields } = this.props;
const { showAdvancedOptions } = this.state; const { showAdvancedOptions } = this.state;
@ -158,9 +151,9 @@ class AppConfigForm extends Component {
} }
render () { render () {
const { fields, handleSubmit, smtpConfigured } = this.props; const { fields, handleSubmit, smtpConfigured, enrollSecret } = this.props;
const { onToggleAdvancedOptions, onToggleRevealSecret, renderAdvancedOptions, renderSmtpSection } = this; const { onToggleAdvancedOptions, renderAdvancedOptions, renderSmtpSection } = this;
const { revealSecret, showAdvancedOptions } = this.state; const { showAdvancedOptions } = this.state;
return ( return (
<form className={baseClass} onSubmit={handleSubmit}> <form className={baseClass} onSubmit={handleSubmit}>
@ -324,16 +317,12 @@ class AppConfigForm extends Component {
</div> </div>
</div> </div>
<div className={`${baseClass}__section`}> <div className={`${baseClass}__section`}>
<h2>Osquery Enrollment Secret</h2> <h2>Osquery Enrollment Secrets</h2>
<div className={`${baseClass}__inputs`}> <div className={`${baseClass}__inputs`}>
<p className={`${baseClass}__enroll-secret-label`}> <p className={`${baseClass}__enroll-secret-label`}>
This is the secret that you use to enroll osquery agents with Fleet: Manage secrets with <code>fleetctl</code>. Active secrets:
<Button variant="unstyled" onClick={onToggleRevealSecret}>Reveal Secret</Button>
</p> </p>
<InputField <EnrollSecretTable secrets={enrollSecret} />
{...fields.osquery_enroll_secret}
type={revealSecret ? 'input' : 'password'}
/>
</div> </div>
</div> </div>
<div className={`${baseClass}__section`}> <div className={`${baseClass}__section`}>

View File

@ -11,6 +11,11 @@ describe('AppConfigForm - form', () => {
formData: { org_name: 'Kolide' }, formData: { org_name: 'Kolide' },
handleSubmit: noop, handleSubmit: noop,
smtpConfigured: false, 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(<AppConfigForm {...defaultProps} />); const form = mount(<AppConfigForm {...defaultProps} />);
@ -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', () => { it('does not render advanced options by default', () => {
expect(form.find({ name: 'domain' }).length).toEqual(0); expect(form.find({ name: 'domain' }).length).toEqual(0);
expect(form.find('Slider').length).toEqual(0); expect(form.find('Slider').length).toEqual(0);

View File

@ -2,62 +2,25 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Button from 'components/buttons/Button'; 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 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'; import certificate from '../../../../assets/images/osquery-certificate.svg';
const baseClass = 'add-host-modal'; const baseClass = 'add-host-modal';
class AddHostModal extends Component { class AddHostModal extends Component {
static propTypes = { static propTypes = {
dispatch: PropTypes.func,
onFetchCertificate: PropTypes.func, onFetchCertificate: PropTypes.func,
onReturnToApp: PropTypes.func, onReturnToApp: PropTypes.func,
osqueryEnrollSecret: PropTypes.string, enrollSecret: enrollSecretInterface,
};
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;
}; };
render() { render() {
const { onCopySecret, toggleSecret } = this;
const { revealSecret } = this.state;
const { const {
onFetchCertificate, onFetchCertificate,
onReturnToApp, onReturnToApp,
osqueryEnrollSecret, enrollSecret,
} = this.props; } = this.props;
return ( return (
@ -66,14 +29,6 @@ class AddHostModal extends Component {
Follow the instructions below to add hosts to your Fleet Instance. Follow the instructions below to add hosts to your Fleet Instance.
</p> </p>
<div className={`${baseClass}__manual-install-header`}>
<Icon name="wrench-hand" />
<h2>Manual Install</h2>
<h3>
Fully Customize Your <strong>Osquery</strong> Installation
</h3>
</div>
<div className={`${baseClass}__manual-install-content`}> <div className={`${baseClass}__manual-install-content`}>
<ol className={`${baseClass}__install-steps`}> <ol className={`${baseClass}__install-steps`}>
<li> <li>
@ -86,38 +41,14 @@ class AddHostModal extends Component {
Fleet / Osquery - Install Docs <Icon name="external-link" /> Fleet / Osquery - Install Docs <Icon name="external-link" />
</a> </a>
</h4> </h4>
<p>
In order to install <strong>osquery</strong> on a client you
will need the following information:
</p>
</li> </li>
<li> <li>
<h4>Retrieve Osquery Enroll Secret</h4> <h4>Osquery Enroll Secret</h4>
<p> <p>
The following is your enroll secret: Provide osquery with one of the following active enroll secrets:
<a
href="#revealSecret"
onClick={toggleSecret}
className={`${baseClass}__reveal-secret`}
>
{revealSecret ? 'Hide' : 'Reveal'} Secret
</a>
</p> </p>
<div className={`${baseClass}__secret-wrapper`}> <div className={`${baseClass}__secret-wrapper`}>
<InputField <EnrollSecretTable secrets={enrollSecret} />
disabled
inputWrapperClass={`${baseClass}__secret-input`}
name="osqueryd-secret"
type={revealSecret ? 'text' : 'password'}
value={osqueryEnrollSecret}
/>
<Button
variant="unstyled"
className={`${baseClass}__secret-copy-icon`}
onClick={onCopySecret(`.${baseClass}__secret-input`)}
>
<Icon name="clipboard" />
</Button>
</div> </div>
</li> </li>
<li> <li>

View File

@ -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(<AddHostModal dispatch={noop} onReturnToApp={noop} />);
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');
});
});

View File

@ -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,
}),
);

View File

@ -16,6 +16,11 @@ export default (client) => {
return client.authenticatedGet(endpoint) return client.authenticatedGet(endpoint)
.then(response => global.window.atob(response.certificate_chain)); .then(response => global.window.atob(response.certificate_chain));
}, },
loadEnrollSecret: () => {
const endpoint = client._endpoint('/v1/kolide/spec/enroll_secret');
return client.authenticatedGet(endpoint);
},
update: (formData) => { update: (formData) => {
const { CONFIG } = endpoints; const { CONFIG } = endpoints;
const configData = helpers.formatConfigDataForServer(formData); const configData = helpers.formatConfigDataForServer(formData);

View File

@ -5,6 +5,7 @@ import { size } from 'lodash';
import AppConfigForm from 'components/forms/admin/AppConfigForm'; import AppConfigForm from 'components/forms/admin/AppConfigForm';
import configInterface from 'interfaces/config'; import configInterface from 'interfaces/config';
import enrollSecretInterface from 'interfaces/enroll_secret';
import deepDifference from 'utilities/deep_difference'; import deepDifference from 'utilities/deep_difference';
import { renderFlash } from 'redux/nodes/notifications/actions'; import { renderFlash } from 'redux/nodes/notifications/actions';
import WarningBanner from 'components/WarningBanner'; import WarningBanner from 'components/WarningBanner';
@ -17,6 +18,7 @@ class AppSettingsPage extends Component {
appConfig: configInterface, appConfig: configInterface,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
enrollSecret: enrollSecretInterface,
}; };
constructor (props) { constructor (props) {
@ -53,7 +55,7 @@ class AppSettingsPage extends Component {
} }
render () { render () {
const { appConfig, error } = this.props; const { appConfig, error, enrollSecret } = this.props;
const { onDismissSmtpWarning, onFormSubmit } = this; const { onDismissSmtpWarning, onFormSubmit } = this;
const { showSmtpWarning } = this.state; const { showSmtpWarning } = this.state;
const { configured: smtpConfigured } = appConfig; const { configured: smtpConfigured } = appConfig;
@ -78,6 +80,7 @@ class AppSettingsPage extends Component {
handleSubmit={onFormSubmit} handleSubmit={onFormSubmit}
serverErrors={error} serverErrors={error}
smtpConfigured={smtpConfigured} smtpConfigured={smtpConfigured}
enrollSecret={enrollSecret}
/> />
</div> </div>
); );
@ -85,9 +88,9 @@ class AppSettingsPage extends Component {
} }
const mapStateToProps = ({ app }) => { 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); export default connect(mapStateToProps)(AppSettingsPage);

View File

@ -7,10 +7,22 @@ import testHelpers from 'test/helpers';
const { connectedComponent, reduxMockStore } = testHelpers; const { connectedComponent, reduxMockStore } = testHelpers;
const baseStore = { 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', () => { describe('AppSettingsPage - component', () => {
afterEach(restoreSpies); afterEach(restoreSpies);

View File

@ -22,6 +22,7 @@ import labelInterface from 'interfaces/label';
import hostInterface from 'interfaces/host'; import hostInterface from 'interfaces/host';
import osqueryTableInterface from 'interfaces/osquery_table'; import osqueryTableInterface from 'interfaces/osquery_table';
import statusLabelsInterface from 'interfaces/status_labels'; import statusLabelsInterface from 'interfaces/status_labels';
import enrollSecretInterface from 'interfaces/enroll_secret';
import { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions'; import { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions';
import { getStatusLabelCounts, setDisplay, silentGetStatusLabelCounts } from 'redux/nodes/components/ManageHostsPage/actions'; import { getStatusLabelCounts, setDisplay, silentGetStatusLabelCounts } from 'redux/nodes/components/ManageHostsPage/actions';
import hostActions from 'redux/nodes/entities/hosts/actions'; import hostActions from 'redux/nodes/entities/hosts/actions';
@ -51,7 +52,7 @@ export class ManageHostsPage extends PureComponent {
labels: PropTypes.arrayOf(labelInterface), labels: PropTypes.arrayOf(labelInterface),
loadingHosts: PropTypes.bool.isRequired, loadingHosts: PropTypes.bool.isRequired,
loadingLabels: PropTypes.bool.isRequired, loadingLabels: PropTypes.bool.isRequired,
osqueryEnrollSecret: PropTypes.string, enrollSecret: enrollSecretInterface,
selectedLabel: labelInterface, selectedLabel: labelInterface,
selectedOsqueryTable: osqueryTableInterface, selectedOsqueryTable: osqueryTableInterface,
statusLabels: statusLabelsInterface, statusLabels: statusLabelsInterface,
@ -356,7 +357,7 @@ export class ManageHostsPage extends PureComponent {
renderAddHostModal = () => { renderAddHostModal = () => {
const { onFetchCertificate, toggleAddHostModal } = this; const { onFetchCertificate, toggleAddHostModal } = this;
const { showAddHostModal } = this.state; const { showAddHostModal } = this.state;
const { dispatch, osqueryEnrollSecret } = this.props; const { enrollSecret } = this.props;
if (!showAddHostModal) { if (!showAddHostModal) {
return false; return false;
@ -369,10 +370,9 @@ export class ManageHostsPage extends PureComponent {
className={`${baseClass}__invite-modal`} className={`${baseClass}__invite-modal`}
> >
<AddHostModal <AddHostModal
dispatch={dispatch}
onFetchCertificate={onFetchCertificate} onFetchCertificate={onFetchCertificate}
onReturnToApp={toggleAddHostModal} onReturnToApp={toggleAddHostModal}
osqueryEnrollSecret={osqueryEnrollSecret} enrollSecret={enrollSecret}
/> />
</Modal> </Modal>
); );
@ -676,7 +676,7 @@ const mapStateToProps = (state, { location, params }) => {
const { selectedOsqueryTable } = state.components.QueryPages; const { selectedOsqueryTable } = state.components.QueryPages;
const { errors: labelErrors, loading: loadingLabels } = state.entities.labels; const { errors: labelErrors, loading: loadingLabels } = state.entities.labels;
const { loading: loadingHosts } = state.entities.hosts; const { loading: loadingHosts } = state.entities.hosts;
const { osquery_enroll_secret: osqueryEnrollSecret } = state.app.config; const enrollSecret = state.app.enrollSecret;
return { return {
display, display,
@ -686,7 +686,7 @@ const mapStateToProps = (state, { location, params }) => {
labels, labels,
loadingHosts, loadingHosts,
loadingLabels, loadingLabels,
osqueryEnrollSecret, enrollSecret,
selectedLabel, selectedLabel,
selectedOsqueryTable, selectedOsqueryTable,
statusLabels, statusLabels,

View File

@ -56,6 +56,7 @@ describe('ManageHostsPage - component', () => {
loadingLabels: false, loadingLabels: false,
selectedOsqueryTable: stubbedOsqueryTable, selectedOsqueryTable: stubbedOsqueryTable,
statusLabels: {}, statusLabels: {},
enrollSecret: [],
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -6,6 +6,9 @@ import { frontendFormattedConfig } from 'redux/nodes/app/helpers';
export const CONFIG_FAILURE = 'CONFIG_FAILURE'; export const CONFIG_FAILURE = 'CONFIG_FAILURE';
export const CONFIG_START = 'CONFIG_START'; export const CONFIG_START = 'CONFIG_START';
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS'; 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 SHOW_BACKGROUND_IMAGE = 'SHOW_BACKGROUND_IMAGE';
export const HIDE_BACKGROUND_IMAGE = 'HIDE_BACKGROUND_IMAGE'; export const HIDE_BACKGROUND_IMAGE = 'HIDE_BACKGROUND_IMAGE';
export const TOGGLE_SMALL_NAV = 'TOGGLE_SMALL_NAV'; export const TOGGLE_SMALL_NAV = 'TOGGLE_SMALL_NAV';
@ -26,6 +29,13 @@ export const loadConfig = { type: CONFIG_START };
export const configSuccess = (data) => { export const configSuccess = (data) => {
return { type: CONFIG_SUCCESS, payload: { 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 = () => { export const getConfig = () => {
return (dispatch) => { return (dispatch) => {
dispatch(loadConfig); 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;
});
};
};

View File

@ -1,6 +1,14 @@
import expect from 'expect'; 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 { configStub } from 'test/stubs';
import { frontendFormattedConfig } from 'redux/nodes/app/helpers'; import { frontendFormattedConfig } from 'redux/nodes/app/helpers';
import Kolide from 'kolide'; import Kolide from 'kolide';
@ -78,4 +86,38 @@ describe('App - actions', () => {
.catch(done); .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);
});
});
}); });

View File

@ -2,6 +2,9 @@ import {
CONFIG_FAILURE, CONFIG_FAILURE,
CONFIG_START, CONFIG_START,
CONFIG_SUCCESS, CONFIG_SUCCESS,
ENROLL_SECRET_FAILURE,
ENROLL_SECRET_START,
ENROLL_SECRET_SUCCESS,
HIDE_BACKGROUND_IMAGE, HIDE_BACKGROUND_IMAGE,
SHOW_BACKGROUND_IMAGE, SHOW_BACKGROUND_IMAGE,
TOGGLE_SMALL_NAV, TOGGLE_SMALL_NAV,
@ -9,6 +12,7 @@ import {
export const initialState = { export const initialState = {
config: {}, config: {},
enrollSecret: [],
error: {}, error: {},
isSmallNav: false, isSmallNav: false,
loading: false, loading: false,
@ -35,6 +39,24 @@ const reducer = (state = initialState, { type, payload }) => {
error: payload.error, error: payload.error,
loading: false, 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: case HIDE_BACKGROUND_IMAGE:
return { return {
...state, ...state,

View File

@ -2,12 +2,15 @@ import expect from 'expect';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
import { import {
loadConfig,
configFailure, configFailure,
configSuccess, configSuccess,
loadEnrollSecret,
enrollSecretFailure,
enrollSecretSuccess,
hideBackgroundImage, hideBackgroundImage,
showBackgroundImage, showBackgroundImage,
toggleSmallNav, toggleSmallNav,
loadConfig,
} from './actions'; } from './actions';
describe('App - reducer', () => { describe('App - reducer', () => {
@ -76,6 +79,7 @@ describe('App - reducer', () => {
}; };
expect(reducer(loadingConfigState, configSuccess(config))).toEqual({ expect(reducer(loadingConfigState, configSuccess(config))).toEqual({
config, config,
enrollSecret: [],
error: {}, error: {},
loading: false, loading: false,
isSmallNav: false, isSmallNav: false,
@ -92,6 +96,52 @@ describe('App - reducer', () => {
loading: true, loading: true,
}; };
expect(reducer(loadingConfigState, configFailure(error))).toEqual({ 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: {}, config: {},
error, error,
loading: false, loading: false,

View File

@ -27,6 +27,12 @@ export const copyText = (elementSelector) => {
return true; 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_SUCCESS = 'Text copied to clipboard';
export const COPY_TEXT_ERROR = 'Text not copied. Please copy manually.'; export const COPY_TEXT_ERROR = 'Text not copied. Please copy manually.';

View File

@ -2,6 +2,7 @@ package datastore
import ( import (
"encoding/json" "encoding/json"
"sort"
"testing" "testing"
"github.com/kolide/fleet/server/kolide" "github.com/kolide/fleet/server/kolide"
@ -75,3 +76,78 @@ func testAdditionalQueries(t *testing.T, ds kolide.Datastore) {
assert.Nil(t, err) assert.Nil(t, err)
assert.JSONEq(t, `{"foo":"bar"}`, string(*info.AdditionalQueries)) 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)
}

View File

@ -16,28 +16,27 @@ import (
) )
var enrollTests = []struct { var enrollTests = []struct {
uuid, hostname, platform string uuid, hostname, platform, nodeKey string
nodeKeySize int
}{ }{
0: {uuid: "6D14C88F-8ECF-48D5-9197-777647BF6B26", 0: {uuid: "6D14C88F-8ECF-48D5-9197-777647BF6B26",
hostname: "web.kolide.co", hostname: "web.kolide.co",
platform: "linux", platform: "linux",
nodeKeySize: 12, nodeKey: "key0",
}, },
1: {uuid: "B998C0EB-38CE-43B1-A743-FBD7A5C9513B", 1: {uuid: "B998C0EB-38CE-43B1-A743-FBD7A5C9513B",
hostname: "mail.kolide.co", hostname: "mail.kolide.co",
platform: "linux", platform: "linux",
nodeKeySize: 10, nodeKey: "key1",
}, },
2: {uuid: "008F0688-5311-4C59-86EE-00C2D6FC3EC2", 2: {uuid: "008F0688-5311-4C59-86EE-00C2D6FC3EC2",
hostname: "home.kolide.co", hostname: "home.kolide.co",
platform: "darwin", platform: "darwin",
nodeKeySize: 25, nodeKey: "key2",
}, },
3: {uuid: "uuid123", 3: {uuid: "uuid123",
hostname: "fakehostname", hostname: "fakehostname",
platform: "darwin", platform: "darwin",
nodeKeySize: 1, nodeKey: "key3",
}, },
} }
@ -259,7 +258,7 @@ func testListHost(t *testing.T, ds kolide.Datastore) {
func testEnrollHost(t *testing.T, ds kolide.Datastore) { func testEnrollHost(t *testing.T, ds kolide.Datastore) {
var hosts []*kolide.Host var hosts []*kolide.Host
for _, tt := range enrollTests { 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) require.Nil(t, err)
hosts = append(hosts, h) hosts = append(hosts, h)
@ -271,7 +270,7 @@ func testEnrollHost(t *testing.T, ds kolide.Datastore) {
func testAuthenticateHost(t *testing.T, ds kolide.Datastore) { func testAuthenticateHost(t *testing.T, ds kolide.Datastore) {
for _, tt := range enrollTests { 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) require.Nil(t, err)
returned, err := ds.AuthenticateHost(h.NodeKey) returned, err := ds.AuthenticateHost(h.NodeKey)

View File

@ -17,7 +17,7 @@ func testLabels(t *testing.T, db kolide.Datastore) {
var host *kolide.Host var host *kolide.Host
var err error var err error
for i := 0; i < 10; i++ { 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") require.Nil(t, err, "enrollment should succeed")
hosts = append(hosts, *host) hosts = append(hosts, *host)
} }

View File

@ -126,7 +126,7 @@ func testHostStatus(t *testing.T, ds kolide.Datastore) {
mockClock := clock.NewMockClock() mockClock := clock.NewMockClock()
h, err := ds.EnrollHost("1", 24) h, err := ds.EnrollHost("1", "key1", "default")
require.Nil(t, err) require.Nil(t, err)
// Make host no longer appear new // Make host no longer appear new

View File

@ -18,6 +18,8 @@ func functionName(f func(*testing.T, kolide.Datastore)) string {
var testFunctions = [...]func(*testing.T, kolide.Datastore){ var testFunctions = [...]func(*testing.T, kolide.Datastore){
testOrgInfo, testOrgInfo,
testAdditionalQueries, testAdditionalQueries,
testEnrollSecrets,
testEnrollSecretRoundtrip,
testCreateInvite, testCreateInvite,
testInviteByEmail, testInviteByEmail,
testInviteByToken, testInviteByToken,

View File

@ -136,7 +136,7 @@ func (d *Datastore) GenerateHostStatusStatistics(now time.Time) (online, offline
return online, offline, mia, new, nil 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() d.mtx.Lock()
defer d.mtx.Unlock() 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") 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{ host := kolide.Host{
OsqueryHostID: osQueryHostID, OsqueryHostID: osQueryHostID,
NodeKey: nodeKey, NodeKey: nodeKey,

View File

@ -5,6 +5,7 @@ import (
"github.com/VividCortex/mysqlerr" "github.com/VividCortex/mysqlerr"
"github.com/go-sql-driver/mysql" "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/kolide/fleet/server/kolide" "github.com/kolide/fleet/server/kolide"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -94,7 +95,6 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error {
org_name, org_name,
org_logo_url, org_logo_url,
kolide_server_url, kolide_server_url,
osquery_enroll_secret,
smtp_configured, smtp_configured,
smtp_sender_address, smtp_sender_address,
smtp_server, smtp_server,
@ -121,12 +121,11 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error {
live_query_disabled, live_query_disabled,
additional_queries additional_queries
) )
VALUES( 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) VALUES( 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
org_name = VALUES(org_name), org_name = VALUES(org_name),
org_logo_url = VALUES(org_logo_url), org_logo_url = VALUES(org_logo_url),
kolide_server_url = VALUES(kolide_server_url), kolide_server_url = VALUES(kolide_server_url),
osquery_enroll_secret = VALUES(osquery_enroll_secret),
smtp_configured = VALUES(smtp_configured), smtp_configured = VALUES(smtp_configured),
smtp_sender_address = VALUES(smtp_sender_address), smtp_sender_address = VALUES(smtp_sender_address),
smtp_server = VALUES(smtp_server), smtp_server = VALUES(smtp_server),
@ -158,7 +157,6 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error {
info.OrgName, info.OrgName,
info.OrgLogoURL, info.OrgLogoURL,
info.KolideServerURL, info.KolideServerURL,
info.EnrollSecret,
info.SMTPConfigured, info.SMTPConfigured,
info.SMTPSenderAddress, info.SMTPSenderAddress,
info.SMTPServer, info.SMTPServer,
@ -188,3 +186,45 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error {
return err 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
}

View File

@ -172,7 +172,8 @@ func (d *Datastore) SaveHost(host *kolide.Host) error {
distributed_interval = ?, distributed_interval = ?,
config_tls_refresh = ?, config_tls_refresh = ?,
logger_tls_period = ?, logger_tls_period = ?,
additional = COALESCE(?, additional) additional = COALESCE(?, additional),
enroll_secret_name = ?
WHERE id = ? WHERE id = ?
` `
err := d.withRetryTxx(func(tx *sqlx.Tx) error { err := d.withRetryTxx(func(tx *sqlx.Tx) error {
@ -205,6 +206,7 @@ func (d *Datastore) SaveHost(host *kolide.Host) error {
host.ConfigTLSRefresh, host.ConfigTLSRefresh,
host.LoggerTLSPeriod, host.LoggerTLSPeriod,
host.Additional, host.Additional,
host.EnrollSecretName,
host.ID, host.ID,
) )
if err != nil { if err != nil {
@ -260,7 +262,6 @@ func (d *Datastore) DeleteHost(hid uint) error {
return nil return nil
} }
// TODO needs test
func (d *Datastore) Host(id uint) (*kolide.Host, error) { func (d *Datastore) Host(id uint) (*kolide.Host, error) {
sqlStatement := ` sqlStatement := `
SELECT * FROM hosts SELECT * FROM hosts
@ -442,24 +443,20 @@ func (d *Datastore) getNetInterfacesForHost(host *kolide.Host) error {
} }
// EnrollHost enrolls a host // 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 == "" { if osqueryHostID == "" {
return nil, fmt.Errorf("missing osquery host identifier") return nil, fmt.Errorf("missing osquery host identifier")
} }
detailUpdateTime := time.Unix(0, 0).Add(24 * time.Hour) 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 := ` sqlInsert := `
INSERT INTO hosts ( INSERT INTO hosts (
detail_update_time, detail_update_time,
osquery_host_id, osquery_host_id,
seen_time, seen_time,
node_key node_key,
) VALUES (?, ?, ?, ?) enroll_secret_name
) VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
node_key = VALUES(node_key), node_key = VALUES(node_key),
deleted = FALSE deleted = FALSE
@ -467,7 +464,7 @@ func (d *Datastore) EnrollHost(osqueryHostID string, nodeKeySize int) (*kolide.H
var result sql.Result 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 { if err != nil {
return nil, errors.Wrap(err, "inserting") return nil, errors.Wrap(err, "inserting")
@ -522,7 +519,8 @@ func (d *Datastore) AuthenticateHost(nodeKey string) (*kolide.Host, error) {
seen_time, seen_time,
distributed_interval, distributed_interval,
logger_tls_period, logger_tls_period,
config_tls_refresh config_tls_refresh,
enroll_secret_name
FROM hosts FROM hosts
WHERE node_key = ? AND NOT deleted WHERE node_key = ? AND NOT deleted
LIMIT 1 LIMIT 1

View File

@ -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
}

View File

@ -3,6 +3,7 @@ package kolide
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"time"
) )
// AppConfigStore contains method for saving and retrieving // AppConfigStore contains method for saving and retrieving
@ -11,6 +12,16 @@ type AppConfigStore interface {
NewAppConfig(info *AppConfig) (*AppConfig, error) NewAppConfig(info *AppConfig) (*AppConfig, error)
AppConfig() (*AppConfig, error) AppConfig() (*AppConfig, error)
SaveAppConfig(info *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 // AppConfigService provides methods for configuring
@ -21,6 +32,12 @@ type AppConfigService interface {
ModifyAppConfig(ctx context.Context, p AppConfigPayload) (info *AppConfig, err error) ModifyAppConfig(ctx context.Context, p AppConfigPayload) (info *AppConfig, err error)
SendTestEmail(ctx context.Context, config *AppConfig) 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. // Certificate returns the PEM encoded certificate chain for osqueryd TLS termination.
// For cases where the connection is self-signed, the server will attempt to // For cases where the connection is self-signed, the server will attempt to
// connect using the InsecureSkipVerify option in tls.Config. // connect using the InsecureSkipVerify option in tls.Config.
@ -84,11 +101,6 @@ type AppConfig struct {
OrgLogoURL string `db:"org_logo_url"` OrgLogoURL string `db:"org_logo_url"`
KolideServerURL string `db:"kolide_server_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 // SMTPConfigured is a flag that indicates if smtp has been successfully
// tested with the settings provided by an admin user. // tested with the settings provided by an admin user.
SMTPConfigured bool `db:"smtp_configured"` SMTPConfigured bool `db:"smtp_configured"`
@ -238,7 +250,6 @@ type OrgInfo struct {
// ServerSettings contains general settings about the kolide App. // ServerSettings contains general settings about the kolide App.
type ServerSettings struct { type ServerSettings struct {
KolideServerURL *string `json:"kolide_server_url,omitempty"` KolideServerURL *string `json:"kolide_server_url,omitempty"`
EnrollSecret *string `json:"osquery_enroll_secret,omitempty"`
LiveQueryDisabled *bool `json:"live_query_disabled,omitempty"` LiveQueryDisabled *bool `json:"live_query_disabled,omitempty"`
} }
@ -272,3 +283,23 @@ type ListOptions struct {
// Direction of ordering // Direction of ordering
OrderDirection OrderDirection 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"`
}

View File

@ -34,12 +34,14 @@ const (
) )
type HostStore interface { type HostStore interface {
// NewHost is deprecated and will be removed. Hosts should always be
// enrolled via EnrollHost.
NewHost(host *Host) (*Host, error) NewHost(host *Host) (*Host, error)
SaveHost(host *Host) error SaveHost(host *Host) error
DeleteHost(hid uint) error DeleteHost(hid uint) error
Host(id uint) (*Host, error) Host(id uint) (*Host, error)
ListHosts(opt ListOptions) ([]*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. // AuthenticateHost authenticates and returns host metadata by node key.
// This method should not return the host "additional" information as this // This method should not return the host "additional" information as this
// is not typically necessary for the operations performed by the osquery // 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"` ConfigTLSRefresh uint `json:"config_tls_refresh" db:"config_tls_refresh"`
LoggerTLSPeriod uint `json:"logger_tls_period" db:"logger_tls_period"` LoggerTLSPeriod uint `json:"logger_tls_period" db:"logger_tls_period"`
Additional *json.RawMessage `json:"additional,omitempty" db:"additional"` 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 // HostSummary is a structure which represents a data summary about the total

View File

@ -12,6 +12,12 @@ type AppConfigFunc func() (*kolide.AppConfig, error)
type SaveAppConfigFunc func(info *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 { type AppConfigStore struct {
NewAppConfigFunc NewAppConfigFunc NewAppConfigFunc NewAppConfigFunc
NewAppConfigFuncInvoked bool NewAppConfigFuncInvoked bool
@ -21,6 +27,15 @@ type AppConfigStore struct {
SaveAppConfigFunc SaveAppConfigFunc SaveAppConfigFunc SaveAppConfigFunc
SaveAppConfigFuncInvoked bool SaveAppConfigFuncInvoked bool
VerifyEnrollSecretFunc VerifyEnrollSecretFunc
VerifyEnrollSecretFuncInvoked bool
ApplyEnrollSecretSpecFunc ApplyEnrollSecretSpecFunc
ApplyEnrollSecretSpecFuncInvoked bool
GetEnrollSecretSpecFunc GetEnrollSecretSpecFunc
GetEnrollSecretSpecFuncInvoked bool
} }
func (s *AppConfigStore) NewAppConfig(info *kolide.AppConfig) (*kolide.AppConfig, error) { 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 s.SaveAppConfigFuncInvoked = true
return s.SaveAppConfigFunc(info) 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()
}

View File

@ -20,7 +20,7 @@ type HostFunc func(id uint) (*kolide.Host, error)
type ListHostsFunc func(opt kolide.ListOptions) ([]*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) 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) 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 s.EnrollHostFuncInvoked = true
return s.EnrollHostFunc(osqueryHostId, nodeKeySize) return s.EnrollHostFunc(osqueryHostId, nodeKey, secretName)
} }
func (s *HostStore) AuthenticateHost(nodeKey string) (*kolide.Host, error) { func (s *HostStore) AuthenticateHost(nodeKey string) (*kolide.Host, error) {

View File

@ -69,3 +69,61 @@ func (c *Client) GetServerSettings() (*kolide.ServerSettings, error) {
} }
return appConfig.ServerSettings, nil 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
}

View File

@ -65,7 +65,6 @@ func makeGetAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint {
}, },
ServerSettings: &kolide.ServerSettings{ ServerSettings: &kolide.ServerSettings{
KolideServerURL: &config.KolideServerURL, KolideServerURL: &config.KolideServerURL,
EnrollSecret: &config.EnrollSecret,
LiveQueryDisabled: &config.LiveQueryDisabled, LiveQueryDisabled: &config.LiveQueryDisabled,
}, },
SMTPSettings: smtpSettings, SMTPSettings: smtpSettings,
@ -93,7 +92,6 @@ func makeModifyAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint {
}, },
ServerSettings: &kolide.ServerSettings{ ServerSettings: &kolide.ServerSettings{
KolideServerURL: &config.KolideServerURL, KolideServerURL: &config.KolideServerURL,
EnrollSecret: &config.EnrollSecret,
LiveQueryDisabled: &config.LiveQueryDisabled, LiveQueryDisabled: &config.LiveQueryDisabled,
}, },
SMTPSettings: smtpSettingsFromAppConfig(config), SMTPSettings: smtpSettingsFromAppConfig(config),
@ -137,3 +135,49 @@ func smtpSettingsFromAppConfig(config *kolide.AppConfig) *kolide.SMTPSettingsPay
SMTPEnableStartTLS: &config.SMTPEnableStartTLS, 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
}
}

View File

@ -3,12 +3,16 @@ package service
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/go-kit/kit/endpoint" "github.com/go-kit/kit/endpoint"
"github.com/kolide/fleet/server/config" "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/contexts/viewer"
"github.com/kolide/fleet/server/datastore/inmem" "github.com/kolide/fleet/server/datastore/inmem"
"github.com/kolide/fleet/server/kolide" "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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -197,25 +201,36 @@ func TestGetNodeKey(t *testing.T) {
} }
func TestAuthenticatedHost(t *testing.T) { func TestAuthenticatedHost(t *testing.T) {
ds, err := inmem.New(config.TestConfig()) ds := new(mock.Store)
require.Nil(t, err)
_, err = ds.NewAppConfig(&kolide.AppConfig{EnrollSecret: "foobarbaz"})
require.Nil(t, err)
svc, err := newTestService(ds, nil) svc, err := newTestService(ds, nil)
require.Nil(t, err) 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( endpoint := authenticatedHost(
svc, svc,
func(ctx context.Context, request interface{}) (interface{}, error) { 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 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 { var authenticatedHostTests = []struct {
nodeKey string nodeKey string
shouldErr bool shouldErr bool

View File

@ -42,9 +42,6 @@ func makeSetupEndpoint(svc kolide.Service) endpoint.Endpoint {
if req.KolideServerURL != nil { if req.KolideServerURL != nil {
configPayload.ServerSettings.KolideServerURL = req.KolideServerURL configPayload.ServerSettings.KolideServerURL = req.KolideServerURL
} }
if req.EnrollSecret != nil {
configPayload.ServerSettings.EnrollSecret = req.EnrollSecret
}
config, err = svc.NewAppConfig(ctx, configPayload) config, err = svc.NewAppConfig(ctx, configPayload)
if err != nil { if err != nil {
return setupResponse{Err: err}, nil return setupResponse{Err: err}, nil
@ -81,7 +78,6 @@ func makeSetupEndpoint(svc kolide.Service) endpoint.Endpoint {
OrgLogoURL: &config.OrgLogoURL, OrgLogoURL: &config.OrgLogoURL,
}, },
KolideServerURL: &config.KolideServerURL, KolideServerURL: &config.KolideServerURL,
EnrollSecret: &config.EnrollSecret,
Token: token, Token: token,
}, nil }, nil
} }

View File

@ -36,6 +36,8 @@ type KolideEndpoints struct {
DeleteSession endpoint.Endpoint DeleteSession endpoint.Endpoint
GetAppConfig endpoint.Endpoint GetAppConfig endpoint.Endpoint
ModifyAppConfig endpoint.Endpoint ModifyAppConfig endpoint.Endpoint
ApplyEnrollSecretSpec endpoint.Endpoint
GetEnrollSecretSpec endpoint.Endpoint
CreateInvite endpoint.Endpoint CreateInvite endpoint.Endpoint
ListInvites endpoint.Endpoint ListInvites endpoint.Endpoint
DeleteInvite 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))), DeleteSession: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteSessionEndpoint(svc))),
GetAppConfig: authenticatedUser(jwtKey, svc, canPerformActions(makeGetAppConfigEndpoint(svc))), GetAppConfig: authenticatedUser(jwtKey, svc, canPerformActions(makeGetAppConfigEndpoint(svc))),
ModifyAppConfig: authenticatedUser(jwtKey, svc, mustBeAdmin(makeModifyAppConfigEndpoint(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))), CreateInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeCreateInviteEndpoint(svc))),
ListInvites: authenticatedUser(jwtKey, svc, mustBeAdmin(makeListInvitesEndpoint(svc))), ListInvites: authenticatedUser(jwtKey, svc, mustBeAdmin(makeListInvitesEndpoint(svc))),
DeleteInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteInviteEndpoint(svc))), DeleteInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteInviteEndpoint(svc))),
@ -225,6 +229,8 @@ type kolideHandlers struct {
DeleteSession http.Handler DeleteSession http.Handler
GetAppConfig http.Handler GetAppConfig http.Handler
ModifyAppConfig http.Handler ModifyAppConfig http.Handler
ApplyEnrollSecretSpec http.Handler
GetEnrollSecretSpec http.Handler
CreateInvite http.Handler CreateInvite http.Handler
ListInvites http.Handler ListInvites http.Handler
DeleteInvite http.Handler DeleteInvite http.Handler
@ -315,6 +321,8 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli
DeleteSession: newServer(e.DeleteSession, decodeDeleteSessionRequest), DeleteSession: newServer(e.DeleteSession, decodeDeleteSessionRequest),
GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest), GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest),
ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest), ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest),
ApplyEnrollSecretSpec: newServer(e.ApplyEnrollSecretSpec, decodeApplyEnrollSecretSpecRequest),
GetEnrollSecretSpec: newServer(e.GetEnrollSecretSpec, decodeNoParamsRequest),
CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest), CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest),
ListInvites: newServer(e.ListInvites, decodeListInvitesRequest), ListInvites: newServer(e.ListInvites, decodeListInvitesRequest),
DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest), 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/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.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/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.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", h.ListInvites).Methods("GET").Name("list_invites")
r.Handle("/api/v1/kolide/invites/{id}", h.DeleteInvite).Methods("DELETE").Name("delete_invite") r.Handle("/api/v1/kolide/invites/{id}", h.DeleteInvite).Methods("DELETE").Name("delete_invite")

View File

@ -36,18 +36,31 @@ func (svc service) NewAppConfig(ctx context.Context, p kolide.AppConfigPayload)
return nil, err return nil, err
} }
fromPayload := appConfigFromAppConfigPayload(p, *config) 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) newConfig, err := svc.ds.NewAppConfig(fromPayload)
if err != nil { if err != nil {
return nil, err 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 return newConfig, nil
} }
@ -117,9 +130,6 @@ func appConfigFromAppConfigPayload(p kolide.AppConfigPayload, config kolide.AppC
if p.ServerSettings != nil && p.ServerSettings.KolideServerURL != nil { if p.ServerSettings != nil && p.ServerSettings.KolideServerURL != nil {
config.KolideServerURL = cleanupURL(*p.ServerSettings.KolideServerURL) 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 { if p.ServerSettings != nil && p.ServerSettings.LiveQueryDisabled != nil {
config.LiveQueryDisabled = *p.ServerSettings.LiveQueryDisabled config.LiveQueryDisabled = *p.ServerSettings.LiveQueryDisabled
} }
@ -229,3 +239,11 @@ func appConfigFromAppConfigPayload(p kolide.AppConfigPayload, config kolide.AppC
} }
return &config 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()
}

View File

@ -4,9 +4,8 @@ import (
"context" "context"
"testing" "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/kolide"
"github.com/kolide/fleet/server/mock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -32,12 +31,14 @@ func TestCleanupURL(t *testing.T) {
} }
func TestCreateAppConfig(t *testing.T) { func TestCreateAppConfig(t *testing.T) {
ds, err := inmem.New(config.TestConfig()) ds := new(mock.Store)
require.Nil(t, err)
require.Nil(t, ds.MigrateData())
svc, err := newTestService(ds, nil) svc, err := newTestService(ds, nil)
require.Nil(t, err) require.Nil(t, err)
ds.AppConfigFunc = func() (*kolide.AppConfig, error) {
return &kolide.AppConfig{}, nil
}
var appConfigTests = []struct { var appConfigTests = []struct {
configPayload kolide.AppConfigPayload configPayload kolide.AppConfigPayload
}{ }{
@ -56,14 +57,30 @@ func TestCreateAppConfig(t *testing.T) {
} }
for _, tt := range appConfigTests { 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) require.Nil(t, err)
payload := tt.configPayload payload := tt.configPayload
assert.NotEmpty(t, result.ID)
assert.Equal(t, *payload.OrgInfo.OrgLogoURL, result.OrgLogoURL) assert.Equal(t, *payload.OrgInfo.OrgLogoURL, result.OrgLogoURL)
assert.Equal(t, *payload.OrgInfo.OrgName, result.OrgName) assert.Equal(t, *payload.OrgInfo.OrgName, result.OrgName)
assert.Equal(t, "https://acme.co:8080", result.KolideServerURL) assert.Equal(t, "https://acme.co:8080", result.KolideServerURL)
assert.Equal(t, *payload.ServerSettings.LiveQueryDisabled, result.LiveQueryDisabled) 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)
} }
} }

View File

@ -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) { 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 { 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 { nodeKey, err := kolide.RandomText(svc.config.Osquery.NodeKeySize)
return "", osqueryError{message: "invalid enroll secret", nodeInvalid: true} 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 { 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 // Save enrollment details if provided

View File

@ -24,42 +24,66 @@ import (
) )
func TestEnrollAgent(t *testing.T) { func TestEnrollAgent(t *testing.T) {
ds, svc, _ := setupOsqueryTests(t) ds := new(mock.Store)
ctx := context.Background() 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{}) svc, err := newTestService(ds, nil)
assert.Nil(t, err) require.Nil(t, err)
assert.Len(t, hosts, 0)
nodeKey, err := svc.EnrollAgent(ctx, "", "host123", nil) nodeKey, err := svc.EnrollAgent(context.Background(), "valid_secret", "host123", nil)
require.Nil(t, err) require.Nil(t, err)
assert.NotEmpty(t, nodeKey) assert.NotEmpty(t, nodeKey)
hosts, err = ds.ListHosts(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, hosts, 1)
} }
func TestEnrollAgentIncorrectEnrollSecret(t *testing.T) { func TestEnrollAgentIncorrectEnrollSecret(t *testing.T) {
ds, svc, _ := setupOsqueryTests(t) ds := new(mock.Store)
ctx := context.Background() 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{}) svc, err := newTestService(ds, nil)
assert.Nil(t, err) require.Nil(t, err)
assert.Len(t, hosts, 0)
nodeKey, err := svc.EnrollAgent(ctx, "not_correct", "host123", nil) nodeKey, err := svc.EnrollAgent(context.Background(), "not_correct", "host123", nil)
assert.NotNil(t, err) assert.NotNil(t, err)
assert.Empty(t, nodeKey) assert.Empty(t, nodeKey)
hosts, err = ds.ListHosts(kolide.ListOptions{})
assert.Nil(t, err)
assert.Len(t, hosts, 0)
} }
func TestEnrollAgentDetails(t *testing.T) { func TestEnrollAgentDetails(t *testing.T) {
ds, svc, _ := setupOsqueryTests(t) ds := new(mock.Store)
ctx := context.Background() 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){ details := map[string](map[string]string){
"osquery_info": {"version": "2.12.0"}, "osquery_info": {"version": "2.12.0"},
@ -73,48 +97,51 @@ func TestEnrollAgentDetails(t *testing.T) {
}, },
"foo": {"foo": "bar"}, "foo": {"foo": "bar"},
} }
nodeKey, err := svc.EnrollAgent(ctx, "", "host123", details) nodeKey, err := svc.EnrollAgent(context.Background(), "", "host123", details)
require.Nil(t, err) require.Nil(t, err)
assert.NotEmpty(t, nodeKey) assert.NotEmpty(t, nodeKey)
hosts, err := ds.ListHosts(kolide.ListOptions{}) assert.Equal(t, "Mac OS X 10.14.5", gotHost.OSVersion)
require.Nil(t, err) assert.Equal(t, "darwin", gotHost.Platform)
require.Len(t, hosts, 1) assert.Equal(t, "2.12.0", gotHost.OsqueryVersion)
assert.Equal(t, "zwass.local", gotHost.HostName)
h := hosts[0] assert.Equal(t, "froobling_uuid", gotHost.UUID)
assert.Equal(t, "Mac OS X 10.14.5", h.OSVersion) assert.Equal(t, "valid", gotHost.EnrollSecretName)
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)
} }
func TestAuthenticateHost(t *testing.T) { func TestAuthenticateHost(t *testing.T) {
ds, svc, mockClock := setupOsqueryTests(t) ds := new(mock.Store)
ctx := context.Background() svc, err := newTestService(ds, nil)
nodeKey, err := svc.EnrollAgent(ctx, "", "host123", nil)
require.Nil(t, err) 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) require.Nil(t, err)
// Verify that the update time is set appropriately ds.AuthenticateHostFunc = func(key string) (*kolide.Host, error) {
checkHost, err := ds.Host(host.ID) return nil, errors.New("not found")
require.Nil(t, err) }
assert.Equal(t, mockClock.Now(), checkHost.UpdatedAt)
// Advance clock time and check that seen time is updated _, err = svc.AuthenticateHost(context.Background(), "test")
mockClock.AddTime(1*time.Minute + 27*time.Second) require.NotNil(t, err)
_, 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)
} }
type testJSONLogger struct { type testJSONLogger struct {
@ -127,18 +154,10 @@ func (n *testJSONLogger) Write(ctx context.Context, logs []json.RawMessage) erro
} }
func TestSubmitStatusLogs(t *testing.T) { func TestSubmitStatusLogs(t *testing.T) {
ds, svc, _ := setupOsqueryTests(t) ds := new(mock.Store)
ctx := context.Background() svc, err := newTestService(ds, nil)
_, err := svc.EnrollAgent(ctx, "", "host123", nil)
require.Nil(t, err) 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 // Hack to get at the service internals and modify the writer
serv := ((svc.(validationMiddleware)).Service).(service) serv := ((svc.(validationMiddleware)).Service).(service)
@ -155,6 +174,8 @@ func TestSubmitStatusLogs(t *testing.T) {
err = json.Unmarshal([]byte(logJSON), &status) err = json.Unmarshal([]byte(logJSON), &status)
require.Nil(t, err) require.Nil(t, err)
host := kolide.Host{}
ctx := hostctx.NewContext(context.Background(), host)
err = serv.SubmitStatusLogs(ctx, status) err = serv.SubmitStatusLogs(ctx, status)
assert.Nil(t, err) assert.Nil(t, err)
@ -162,18 +183,10 @@ func TestSubmitStatusLogs(t *testing.T) {
} }
func TestSubmitResultLogs(t *testing.T) { func TestSubmitResultLogs(t *testing.T) {
ds, svc, _ := setupOsqueryTests(t) ds := new(mock.Store)
ctx := context.Background() svc, err := newTestService(ds, nil)
_, err := svc.EnrollAgent(ctx, "", "host123", nil)
require.Nil(t, err) 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 // Hack to get at the service internals and modify the writer
serv := ((svc.(validationMiddleware)).Service).(service) serv := ((svc.(validationMiddleware)).Service).(service)
@ -194,6 +207,8 @@ func TestSubmitResultLogs(t *testing.T) {
err = json.Unmarshal([]byte(logJSON), &results) err = json.Unmarshal([]byte(logJSON), &results)
require.Nil(t, err) require.Nil(t, err)
host := kolide.Host{}
ctx := hostctx.NewContext(context.Background(), host)
err = serv.SubmitResultLogs(ctx, results) err = serv.SubmitResultLogs(ctx, results)
assert.Nil(t, err) assert.Nil(t, err)
@ -496,16 +511,23 @@ func TestGetClientConfig(t *testing.T) {
} }
func TestDetailQueriesWithEmptyStrings(t *testing.T) { func TestDetailQueriesWithEmptyStrings(t *testing.T) {
ds, svc, mockClock := setupOsqueryTests(t) ds := new(mock.Store)
ctx := context.Background() mockClock := clock.NewMockClock()
svc, err := newTestServiceWithClock(ds, nil, mockClock)
nodeKey, err := svc.EnrollAgent(ctx, "", "host123", nil)
assert.Nil(t, err)
host, err := ds.AuthenticateHost(nodeKey)
require.Nil(t, err) 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 // With a new host, we should get the detail queries (and accelerated
// queries) // queries)
@ -606,39 +628,41 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) {
err = json.Unmarshal([]byte(resultJSON), &results) err = json.Unmarshal([]byte(resultJSON), &results)
require.Nil(t, err) 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 // Verify that results are ingested properly
svc.SubmitDistributedQueryResults(ctx, results, map[string]kolide.OsqueryStatus{}) 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 // osquery_info
assert.Equal(t, "darwin", host.Platform) assert.Equal(t, "darwin", gotHost.Platform)
assert.Equal(t, "1.8.2", host.OsqueryVersion) assert.Equal(t, "1.8.2", gotHost.OsqueryVersion)
// system_info // system_info
assert.Equal(t, 17179869184, host.PhysicalMemory) assert.Equal(t, 17179869184, gotHost.PhysicalMemory)
assert.Equal(t, "computer.local", host.HostName) assert.Equal(t, "computer.local", gotHost.HostName)
assert.Equal(t, "uuid", host.UUID) assert.Equal(t, "uuid", gotHost.UUID)
// os_version // 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 // uptime
assert.Equal(t, 1730893*time.Second, host.Uptime) assert.Equal(t, 1730893*time.Second, gotHost.Uptime)
// osquery_flags // osquery_flags
assert.Equal(t, uint(0), host.ConfigTLSRefresh) assert.Equal(t, uint(0), gotHost.ConfigTLSRefresh)
assert.Equal(t, uint(0), host.DistributedInterval) assert.Equal(t, uint(0), gotHost.DistributedInterval)
assert.Equal(t, uint(0), host.LoggerTLSPeriod) assert.Equal(t, uint(0), gotHost.LoggerTLSPeriod)
host.HostName = "computer.local"
host.DetailUpdateTime = mockClock.Now()
mockClock.AddTime(1 * time.Minute) mockClock.AddTime(1 * time.Minute)
// Now no detail queries should be required // Now no detail queries should be required
host, err = ds.AuthenticateHost(nodeKey) ctx = hostctx.NewContext(context.Background(), host)
require.Nil(t, err)
ctx = hostctx.NewContext(ctx, *host)
queries, acc, err = svc.GetDistributedQueries(ctx) queries, acc, err = svc.GetDistributedQueries(ctx)
assert.Nil(t, err) assert.Nil(t, err)
assert.Len(t, queries, 0) assert.Len(t, queries, 0)
@ -647,12 +671,6 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) {
// Advance clock and queries should exist again // Advance clock and queries should exist again
mockClock.AddTime(1*time.Hour + 1*time.Minute) 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) queries, acc, err = svc.GetDistributedQueries(ctx)
assert.Nil(t, err) assert.Nil(t, err)
assert.Len(t, queries, len(detailQueries)) assert.Len(t, queries, len(detailQueries))
@ -660,16 +678,23 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) {
} }
func TestDetailQueries(t *testing.T) { func TestDetailQueries(t *testing.T) {
ds, svc, mockClock := setupOsqueryTests(t) ds := new(mock.Store)
ctx := context.Background() mockClock := clock.NewMockClock()
svc, err := newTestServiceWithClock(ds, nil, mockClock)
nodeKey, err := svc.EnrollAgent(ctx, "", "host123", nil)
assert.Nil(t, err)
host, err := ds.AuthenticateHost(nodeKey)
require.Nil(t, err) 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 // With a new host, we should get the detail queries (and accelerated
// queries) // queries)
@ -774,39 +799,40 @@ func TestDetailQueries(t *testing.T) {
err = json.Unmarshal([]byte(resultJSON), &results) err = json.Unmarshal([]byte(resultJSON), &results)
require.Nil(t, err) 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 // Verify that results are ingested properly
svc.SubmitDistributedQueryResults(ctx, results, map[string]kolide.OsqueryStatus{}) 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 // osquery_info
assert.Equal(t, "darwin", host.Platform) assert.Equal(t, "darwin", gotHost.Platform)
assert.Equal(t, "1.8.2", host.OsqueryVersion) assert.Equal(t, "1.8.2", gotHost.OsqueryVersion)
// system_info // system_info
assert.Equal(t, 17179869184, host.PhysicalMemory) assert.Equal(t, 17179869184, gotHost.PhysicalMemory)
assert.Equal(t, "computer.local", host.HostName) assert.Equal(t, "computer.local", gotHost.HostName)
assert.Equal(t, "uuid", host.UUID) assert.Equal(t, "uuid", gotHost.UUID)
// os_version // 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 // uptime
assert.Equal(t, 1730893*time.Second, host.Uptime) assert.Equal(t, 1730893*time.Second, gotHost.Uptime)
// osquery_flags // osquery_flags
assert.Equal(t, uint(10), host.ConfigTLSRefresh) assert.Equal(t, uint(10), gotHost.ConfigTLSRefresh)
assert.Equal(t, uint(5), host.DistributedInterval) assert.Equal(t, uint(5), gotHost.DistributedInterval)
assert.Equal(t, uint(60), host.LoggerTLSPeriod) assert.Equal(t, uint(60), gotHost.LoggerTLSPeriod)
host.HostName = "computer.local"
host.DetailUpdateTime = mockClock.Now()
mockClock.AddTime(1 * time.Minute) mockClock.AddTime(1 * time.Minute)
// Now no detail queries should be required // Now no detail queries should be required
host, err = ds.AuthenticateHost(nodeKey) ctx = hostctx.NewContext(ctx, host)
require.Nil(t, err)
ctx = hostctx.NewContext(ctx, *host)
queries, acc, err = svc.GetDistributedQueries(ctx) queries, acc, err = svc.GetDistributedQueries(ctx)
assert.Nil(t, err) assert.Nil(t, err)
assert.Len(t, queries, 0) assert.Len(t, queries, 0)
@ -815,12 +841,6 @@ func TestDetailQueries(t *testing.T) {
// Advance clock and queries should exist again // Advance clock and queries should exist again
mockClock.AddTime(1*time.Hour + 1*time.Minute) 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) queries, acc, err = svc.GetDistributedQueries(ctx)
assert.Nil(t, err) assert.Nil(t, err)
assert.Len(t, queries, len(detailQueries)) assert.Len(t, queries, len(detailQueries))
@ -995,63 +1015,49 @@ func TestDistributedQueryResults(t *testing.T) {
} }
func TestOrphanedQueryCampaign(t *testing.T) { func TestOrphanedQueryCampaign(t *testing.T) {
ds, err := inmem.New(config.TestConfig()) ds := new(mock.Store)
require.Nil(t, err)
_, err = ds.NewAppConfig(&kolide.AppConfig{EnrollSecret: ""})
require.Nil(t, err)
rs := pubsub.NewInmemQueryResults() rs := pubsub.NewInmemQueryResults()
svc, err := newTestService(ds, rs) svc, err := newTestService(ds, rs)
require.Nil(t, err) 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) savedCampaign := &kolide.DistributedQueryCampaign{}
require.Nil(t, err) ds.SaveDistributedQueryCampaignFunc = func(campaign *kolide.DistributedQueryCampaign) error {
savedCampaign = campaign
host, err := ds.AuthenticateHost(nodeKey) return nil
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)
// Submit results
queryKey := hostDistributedQueryPrefix + "1"
expectedRows := []map[string]string{ expectedRows := []map[string]string{
{ map[string]string{
"year": "2016", "foo": "bar",
"month": "11", },
"day": "11", map[string]string{
"hour": "6", "baz": "boom",
"minutes": "12",
"seconds": "10",
}, },
} }
host := kolide.Host{HostName: "the fooer"}
results := map[string][]map[string]string{ results := map[string][]map[string]string{
queryKey: expectedRows, queryKey: expectedRows,
} }
// Submit results ctx := context.Background()
ctx = hostctx.NewContext(context.Background(), *host) ctx = hostctx.NewContext(context.Background(), host)
err = svc.SubmitDistributedQueryResults(ctx, results, map[string]kolide.OsqueryStatus{}) err = svc.SubmitDistributedQueryResults(ctx, results, map[string]kolide.OsqueryStatus{})
require.Nil(t, err) require.Nil(t, err)
// The campaign should be set to completed because it is orphaned // Ensure that status is changed to completed when there is no listener for
campaign, err = ds.DistributedQueryCampaign(campaign.ID) // results.
require.Nil(t, err) require.NotNil(t, savedCampaign)
assert.Equal(t, kolide.QueryComplete, campaign.Status) assert.Equal(t, kolide.QueryComplete, savedCampaign.Status)
} }
func TestUpdateHostIntervals(t *testing.T) { 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()) ds, err := inmem.New(config.TestConfig())
require.Nil(t, err) require.Nil(t, err)
_, err = ds.NewAppConfig(&kolide.AppConfig{EnrollSecret: ""})
require.Nil(t, err)
mockClock := clock.NewMockClock() mockClock := clock.NewMockClock()
svc, err := newTestServiceWithClock(ds, nil, mockClock) svc, err := newTestServiceWithClock(ds, nil, mockClock)
require.Nil(t, err) require.Nil(t, err)

View File

@ -15,3 +15,12 @@ func decodeModifyAppConfigRequest(ctx context.Context, r *http.Request) (interfa
} }
return appConfigRequest{Payload: payload}, nil 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
}