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
Options *kolide.OptionsSpec
AppConfig *kolide.AppConfigPayload
EnrollSecret *kolide.EnrollSecretSpec
}
func specGroupFromBytes(b []byte) (*specGroup, error) {
@ -91,6 +92,17 @@ func specGroupFromBytes(b []byte) (*specGroup, error) {
}
specs.AppConfig = appConfigSpec
case "enroll_secret":
if specs.AppConfig != nil {
return nil, errors.New("enroll_secret defined twice in the same file")
}
var enrollSecretSpec *kolide.EnrollSecretSpec
if err := yaml.Unmarshal(s.Spec, &enrollSecretSpec); err != nil {
return nil, errors.Wrap(err, "unmarshaling enroll secret spec")
}
specs.EnrollSecret = enrollSecretSpec
default:
return nil, errors.Errorf("unknown kind %q", s.Kind)
}
@ -181,6 +193,14 @@ func applyCommand() cli.Command {
}
if specs.EnrollSecret != nil {
if err := fleet.ApplyEnrollSecretSpec(specs.EnrollSecret); err != nil {
return errors.Wrap(err, "applying enroll secrets")
}
fmt.Printf("[+] applied enroll secrets\n")
}
return nil
},
}

View File

@ -392,8 +392,9 @@ func getOptionsCommand() cli.Command {
func getEnrollSecretCommand() cli.Command {
return cli.Command{
Name: "enroll-secret",
Usage: "Retrieve the osquery enroll secret",
Name: "enroll_secret",
Aliases: []string{"enroll_secrets", "enroll-secret", "enroll-secrets"},
Usage: "Retrieve the osquery enroll secrets",
Flags: []cli.Flag{
configFlag(),
contextFlag(),
@ -404,16 +405,23 @@ func getEnrollSecretCommand() cli.Command {
return err
}
settings, err := fleet.GetServerSettings()
secrets, err := fleet.GetEnrollSecretSpec()
if err != nil {
return err
}
if settings == nil {
return errors.New("error: server setting were nil")
spec := specGeneric{
Kind: "enroll_secret",
Version: kolide.ApiVersion,
Spec: secrets,
}
fmt.Println(*settings.EnrollSecret)
b, err := yaml.Marshal(spec)
if err != nil {
return err
}
fmt.Print(string(b))
return nil
},
}

View File

@ -255,7 +255,6 @@ spec:
org_name: Example Org
server_settings:
kolide_server_url: https://fleet.example.org:8080
osquery_enroll_secret: supersekretsecret
smtp_settings:
authentication_method: authmethod_plain
authentication_type: authtype_username_password
@ -291,3 +290,25 @@ The following options are available when configuring SMTP authentication:
- `authmethod_cram_md5`
- `authmethod_login`
- `authmethod_plain`
## Enroll Secrets
The following file shows how to configure enroll secrets. Note that secrets can be changed or made inactive, but not deleted. Hosts may not enroll with inactive secrets.
The name of the enroll secret used to authenticate is stored with the host and is included with API results.
```yaml
apiVersion: v1
kind: enroll_secret
spec:
secrets:
- active: true
name: default
secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff
- active: true
name: new_one
secret: reallyworks
- active: false
name: inactive_secret
secret: thissecretwontwork!
```

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
- `--root_directory`: the location of the local database, pidfiles, etc.
- `--enroll_secret`: the enroll secret you generated above for your environment
- `--enroll_secret`: the enroll secret to authenticate hosts with Fleet
(retrieve from Fleet UI or `fleetctl get enroll_secret`)
```
./build/launcher \
@ -45,7 +46,8 @@ $ ./build/package-builder make \
As you can see, to generate a Launcher package, you need only call `package-builder make` with two command-line arguments:
- `--hostname`: the hostname of the gRPC server for your environment
- `--enroll_secret`: the enroll secret you generated above for your environment
- `--enroll_secret`: the enroll secret to authenticate hosts with Fleet
(retrieve from Fleet UI or `fleetctl get enroll_secret`)
You can also add the `--mac_package_signing_key` flag to define the name of the macOS package signing key name that you'd like to use to sign the macOS packages. For example:
@ -61,7 +63,7 @@ You can find various ways to install osquery on a variety of platforms at https:
#### Set an environment variable with an agent enrollment secret
The enrollment secret is a value that osquery uses to ensure a level of confidence that the host running osquery is actually a host that you would like to hear from. There are a few ways you can set the enrollment secret on the hosts which you control. You can either set the value as:
The enrollment secret is a value that osquery provides to authenticate with Fleet. There are a few ways you can set the enrollment secret on the hosts which you control. You can either set the value as:
- an value of an environment variable (a common name is `OSQUERY_ENROLL_SECRET`)
- the content of a local file (a common path is `/etc/osquery/enrollment_secret`)
@ -69,7 +71,10 @@ The enrollment secret is a value that osquery uses to ensure a level of confiden
The value of the environment variable or content of the file should be a secret shared between the osqueryd client and the Fleet server. This is basically osqueryd's passphrase which it uses to authenticate with Fleet, convincing Fleet that it is actually one of your hosts. The passphrase could be whatever you'd like, but it would be prudent to have the passphrase long, complex, mixed-case, etc. When you launch the Fleet server, you should specify this same value.
If you use an environment variable for this, you can specify it with the `--enroll_secret_env` flag when you launch osqueryd. If you use a local file for this, you can specify it's path with the `--enroll_secret_path` flag.
s
To retrieve the enroll secret, use the "Add New Host" dialog in the Fleet UI or
`fleetctl get enroll_secret`).
If your organization has a robust internal public key infrastructure (PKI) and you already deploy TLS client certificates to each host to uniquely identify them, then osquery supports an advanced authentication mechanism which takes advantage of this. Fleet can be fronted with a proxy that will perform the TLS client authentication.
#### Deploy the TLS certificate that osquery will use to communicate with Fleet
@ -140,3 +145,11 @@ Note that osqueryd requires a full certificate chain, even for certificates whic
Once you've configured the `config.mk` file with the correct variables, you can run `make` in the `tools/mac` directory. Running `make` will create a new `kolide-enroll.pkg` file which you can import into your software repository and deploy to your mac fleet.
The enrollment package must installed after the osqueryd package, and will install a LaunchDaemon to keep the osqueryd process running.
## Multiple Enroll Secrets
Multiple enroll secrets can be set to allow different groups of hosts to
authenticate with Fleet. When a host enrolls, the corresponding enroll secret is
recorded and can be used to segment hosts.
To set the enroll secret, use the `fleetctl` tool to apply an [enroll secret spec](../cli/file-format.md#enroll-secrets)

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"
---
apiVersion: v1
kind: enroll_secret
spec:
secrets:
- active: true
name: default
secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff
- active: true
name: new_one
secret: reallyworks
- active: false
name: inactive_secret
secret: thissecretwontwork!
---
apiVersion: v1
kind: label
spec:
name: pending_updates

View File

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

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,
error: PropTypes.string,
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.node, PropTypes.string]),
label: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
label: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.node]),
name: PropTypes.string,
type: PropTypes.string,
};

View File

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

View File

@ -11,6 +11,11 @@ describe('AppConfigForm - form', () => {
formData: { org_name: 'Kolide' },
handleSubmit: noop,
smtpConfigured: false,
enrollSecret: [
{ name: 'foo', secret: 'foo_secret', active: true },
{ name: 'bar', secret: 'bar_secret', active: true },
{ name: 'inactive', secret: 'inactive', active: false },
],
};
const form = mount(<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', () => {
expect(form.find({ name: 'domain' }).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 Button from 'components/buttons/Button';
import enrollSecretInterface from 'interfaces/enroll_secret';
import EnrollSecretTable from 'components/config/EnrollSecretTable';
import Icon from 'components/icons/Icon';
import InputField from 'components/forms/fields/InputField';
import { renderFlash } from 'redux/nodes/notifications/actions';
import {
copyText,
COPY_TEXT_SUCCESS,
COPY_TEXT_ERROR,
} from 'utilities/copy_text';
import certificate from '../../../../assets/images/osquery-certificate.svg';
const baseClass = 'add-host-modal';
class AddHostModal extends Component {
static propTypes = {
dispatch: PropTypes.func,
onFetchCertificate: PropTypes.func,
onReturnToApp: PropTypes.func,
osqueryEnrollSecret: PropTypes.string,
};
constructor(props) {
super(props);
this.state = { revealSecret: false };
}
onCopySecret = (elementClass) => {
return (evt) => {
evt.preventDefault();
const { dispatch } = this.props;
if (copyText(elementClass)) {
dispatch(renderFlash('success', COPY_TEXT_SUCCESS));
} else {
this.setState({ revealSecret: true });
dispatch(renderFlash('error', COPY_TEXT_ERROR));
}
};
};
toggleSecret = (evt) => {
const { revealSecret } = this.state;
evt.preventDefault();
this.setState({ revealSecret: !revealSecret });
return false;
enrollSecret: enrollSecretInterface,
};
render() {
const { onCopySecret, toggleSecret } = this;
const { revealSecret } = this.state;
const {
onFetchCertificate,
onReturnToApp,
osqueryEnrollSecret,
enrollSecret,
} = this.props;
return (
@ -66,14 +29,6 @@ class AddHostModal extends Component {
Follow the instructions below to add hosts to your Fleet Instance.
</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`}>
<ol className={`${baseClass}__install-steps`}>
<li>
@ -86,38 +41,14 @@ class AddHostModal extends Component {
Fleet / Osquery - Install Docs <Icon name="external-link" />
</a>
</h4>
<p>
In order to install <strong>osquery</strong> on a client you
will need the following information:
</p>
</li>
<li>
<h4>Retrieve Osquery Enroll Secret</h4>
<h4>Osquery Enroll Secret</h4>
<p>
The following is your enroll secret:
<a
href="#revealSecret"
onClick={toggleSecret}
className={`${baseClass}__reveal-secret`}
>
{revealSecret ? 'Hide' : 'Reveal'} Secret
</a>
Provide osquery with one of the following active enroll secrets:
</p>
<div className={`${baseClass}__secret-wrapper`}>
<InputField
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>
<EnrollSecretTable secrets={enrollSecret} />
</div>
</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)
.then(response => global.window.atob(response.certificate_chain));
},
loadEnrollSecret: () => {
const endpoint = client._endpoint('/v1/kolide/spec/enroll_secret');
return client.authenticatedGet(endpoint);
},
update: (formData) => {
const { CONFIG } = endpoints;
const configData = helpers.formatConfigDataForServer(formData);

View File

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

View File

@ -7,10 +7,22 @@ import testHelpers from 'test/helpers';
const { connectedComponent, reduxMockStore } = testHelpers;
const baseStore = {
app: { config: flatConfigStub },
app: { config: flatConfigStub, enrollSecret: [] },
};
const storeWithoutSMTPConfig = {
...baseStore,
app: {
config: { ...flatConfigStub, configured: false },
enrollSecret: [],
},
};
const storeWithSMTPConfig = {
...baseStore,
app: {
config: { ...flatConfigStub, configured: true },
enrollSecret: [],
},
};
const storeWithoutSMTPConfig = { ...baseStore, app: { config: { ...flatConfigStub, configured: false } } };
const storeWithSMTPConfig = { ...baseStore, app: { config: { ...flatConfigStub, configured: true } } };
describe('AppSettingsPage - component', () => {
afterEach(restoreSpies);

View File

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

View File

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

View File

@ -6,6 +6,9 @@ import { frontendFormattedConfig } from 'redux/nodes/app/helpers';
export const CONFIG_FAILURE = 'CONFIG_FAILURE';
export const CONFIG_START = 'CONFIG_START';
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
export const ENROLL_SECRET_FAILURE = 'ENROLL_SECRET_FAILURE';
export const ENROLL_SECRET_START = 'ENROLL_SECRET_START';
export const ENROLL_SECRET_SUCCESS = 'ENROLL_SECRET_SUCCESS';
export const SHOW_BACKGROUND_IMAGE = 'SHOW_BACKGROUND_IMAGE';
export const HIDE_BACKGROUND_IMAGE = 'HIDE_BACKGROUND_IMAGE';
export const TOGGLE_SMALL_NAV = 'TOGGLE_SMALL_NAV';
@ -26,6 +29,13 @@ export const loadConfig = { type: CONFIG_START };
export const configSuccess = (data) => {
return { type: CONFIG_SUCCESS, payload: { data } };
};
export const enrollSecretFailure = (error) => {
return { type: ENROLL_SECRET_FAILURE, payload: { error } };
};
export const loadEnrollSecret = { type: ENROLL_SECRET_START };
export const enrollSecretSuccess = (data) => {
return { type: ENROLL_SECRET_SUCCESS, payload: { data } };
};
export const getConfig = () => {
return (dispatch) => {
dispatch(loadConfig);
@ -67,3 +77,22 @@ export const updateConfig = (configData) => {
});
};
};
export const getEnrollSecret = () => {
return (dispatch) => {
dispatch(loadEnrollSecret);
return Kolide.config.loadEnrollSecret()
.then((secret) => {
dispatch(enrollSecretSuccess(secret.specs.secrets));
return secret;
})
.catch((error) => {
const formattedErrors = formatApiErrors(error);
dispatch(enrollSecretFailure(formattedErrors));
throw formattedErrors;
});
};
};

View File

@ -1,6 +1,14 @@
import expect from 'expect';
import { CONFIG_START, CONFIG_SUCCESS, getConfig, updateConfig } from 'redux/nodes/app/actions';
import {
CONFIG_START,
CONFIG_SUCCESS,
ENROLL_SECRET_START,
ENROLL_SECRET_SUCCESS,
getConfig,
updateConfig,
getEnrollSecret,
} from 'redux/nodes/app/actions';
import { configStub } from 'test/stubs';
import { frontendFormattedConfig } from 'redux/nodes/app/helpers';
import Kolide from 'kolide';
@ -78,4 +86,38 @@ describe('App - actions', () => {
.catch(done);
});
});
describe('getEnrollSecret action', () => {
const store = reduxMockStore({});
it('calls the api enrollSecret endpoint', (done) => {
const bearerToken = 'abc123';
const request = configMocks.loadAll.valid(bearerToken);
Kolide.setBearerToken(bearerToken);
store.dispatch(getEnrollSecret())
.then(() => {
expect(request.isDone()).toEqual(true);
done();
})
.catch(done);
});
it('dispatches ENROLLSECRET_START & ENROLLSECRET_SUCCESS actions', (done) => {
const bearerToken = 'abc123';
configMocks.loadAll.valid(bearerToken);
Kolide.setBearerToken(bearerToken);
store.dispatch(getEnrollSecret())
.then(() => {
const actions = store.getActions()
.map((action) => { return action.type; });
expect(actions).toInclude(ENROLL_SECRET_START);
expect(actions).toInclude(ENROLL_SECRET_SUCCESS);
done();
})
.catch(done);
});
});
});

View File

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

View File

@ -2,12 +2,15 @@ import expect from 'expect';
import reducer, { initialState } from './reducer';
import {
loadConfig,
configFailure,
configSuccess,
loadEnrollSecret,
enrollSecretFailure,
enrollSecretSuccess,
hideBackgroundImage,
showBackgroundImage,
toggleSmallNav,
loadConfig,
} from './actions';
describe('App - reducer', () => {
@ -76,6 +79,7 @@ describe('App - reducer', () => {
};
expect(reducer(loadingConfigState, configSuccess(config))).toEqual({
config,
enrollSecret: [],
error: {},
loading: false,
isSmallNav: false,
@ -92,6 +96,52 @@ describe('App - reducer', () => {
loading: true,
};
expect(reducer(loadingConfigState, configFailure(error))).toEqual({
config: {},
enrollSecret: [],
error,
loading: false,
isSmallNav: false,
showBackgroundImage: false,
});
});
});
context('loadEnrollSecret action', () => {
it('sets the state to loading', () => {
expect(reducer(initialState, loadEnrollSecret)).toEqual({
...initialState,
loading: true,
});
});
});
context('enrollSecretSuccess action', () => {
it('sets the enrollSecret in state', () => {
const enrollSecret = [{ name: 'Kolide' }];
const loadingEnrollSecretState = {
...initialState,
loading: true,
};
expect(reducer(loadingEnrollSecretState, enrollSecretSuccess(enrollSecret))).toEqual({
enrollSecret,
config: {},
error: {},
loading: false,
isSmallNav: false,
showBackgroundImage: false,
});
});
});
context('enrollSecretFailure action', () => {
it('sets the error in state', () => {
const error = 'Unable to get enrollSecret';
const loadingEnrollSecretState = {
...initialState,
loading: true,
};
expect(reducer(loadingEnrollSecretState, enrollSecretFailure(error))).toEqual({
enrollSecret: [],
config: {},
error,
loading: false,

View File

@ -27,6 +27,12 @@ export const copyText = (elementSelector) => {
return true;
};
export const stringToClipboard = (string) => {
const { navigator } = global;
return navigator.clipboard.writeText(string);
};
export const COPY_TEXT_SUCCESS = 'Text copied to clipboard';
export const COPY_TEXT_ERROR = 'Text not copied. Please copy manually.';

View File

@ -2,6 +2,7 @@ package datastore
import (
"encoding/json"
"sort"
"testing"
"github.com/kolide/fleet/server/kolide"
@ -75,3 +76,78 @@ func testAdditionalQueries(t *testing.T, ds kolide.Datastore) {
assert.Nil(t, err)
assert.JSONEq(t, `{"foo":"bar"}`, string(*info.AdditionalQueries))
}
func testEnrollSecrets(t *testing.T, ds kolide.Datastore) {
name, err := ds.VerifyEnrollSecret("missing")
assert.Error(t, err)
assert.Empty(t, name)
err = ds.ApplyEnrollSecretSpec(
&kolide.EnrollSecretSpec{
Secrets: []kolide.EnrollSecret{
kolide.EnrollSecret{Name: "one", Secret: "one_secret", Active: true},
kolide.EnrollSecret{Name: "two", Secret: "two_secret", Active: false},
},
},
)
assert.NoError(t, err)
name, err = ds.VerifyEnrollSecret("one")
assert.Error(t, err, "secret should not match")
assert.Empty(t, name, "secret name should be empty")
name, err = ds.VerifyEnrollSecret("one_secret")
assert.NoError(t, err)
assert.Equal(t, "one", name)
name, err = ds.VerifyEnrollSecret("two_secret")
assert.Error(t, err)
assert.Equal(t, "", name)
err = ds.ApplyEnrollSecretSpec(
&kolide.EnrollSecretSpec{
Secrets: []kolide.EnrollSecret{
kolide.EnrollSecret{Name: "one", Secret: "one_secret", Active: false},
kolide.EnrollSecret{Name: "two", Secret: "two_secret", Active: true},
},
},
)
assert.NoError(t, err)
name, err = ds.VerifyEnrollSecret("one_secret")
assert.Error(t, err)
assert.Equal(t, "", name)
name, err = ds.VerifyEnrollSecret("two_secret")
assert.NoError(t, err)
assert.Equal(t, "two", name)
}
func testEnrollSecretRoundtrip(t *testing.T, ds kolide.Datastore) {
spec, err := ds.GetEnrollSecretSpec()
require.NoError(t, err)
assert.Len(t, spec.Secrets, 1)
expectedSpec := kolide.EnrollSecretSpec{
Secrets: []kolide.EnrollSecret{
kolide.EnrollSecret{Name: "one", Secret: "one_secret", Active: false},
kolide.EnrollSecret{Name: "two", Secret: "two_secret", Active: true},
},
}
err = ds.ApplyEnrollSecretSpec(&expectedSpec)
require.NoError(t, err)
spec, err = ds.GetEnrollSecretSpec()
require.NoError(t, err)
require.Len(t, spec.Secrets, 3)
// sort secrets before equality checks to ensure proper order
sort.Slice(spec.Secrets, func(i, j int) bool { return spec.Secrets[i].Name < spec.Secrets[j].Name })
assert.Equal(t, "default", spec.Secrets[0].Name)
assert.Equal(t, "one", spec.Secrets[1].Name)
assert.Equal(t, "one_secret", spec.Secrets[1].Secret)
assert.Equal(t, false, spec.Secrets[1].Active)
assert.Equal(t, "two", spec.Secrets[2].Name)
assert.Equal(t, "two_secret", spec.Secrets[2].Secret)
assert.Equal(t, true, spec.Secrets[2].Active)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -136,7 +136,7 @@ func (d *Datastore) GenerateHostStatusStatistics(now time.Time) (online, offline
return online, offline, mia, new, nil
}
func (d *Datastore) EnrollHost(osQueryHostID string, nodeKeySize int) (*kolide.Host, error) {
func (d *Datastore) EnrollHost(osQueryHostID, nodeKey, secretName string) (*kolide.Host, error) {
d.mtx.Lock()
defer d.mtx.Unlock()
@ -144,11 +144,6 @@ func (d *Datastore) EnrollHost(osQueryHostID string, nodeKeySize int) (*kolide.H
return nil, errors.New("missing host identifier from osquery for host enrollment")
}
nodeKey, err := kolide.RandomText(nodeKeySize)
if err != nil {
return nil, err
}
host := kolide.Host{
OsqueryHostID: osQueryHostID,
NodeKey: nodeKey,

View File

@ -5,6 +5,7 @@ import (
"github.com/VividCortex/mysqlerr"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/kolide/fleet/server/kolide"
"github.com/pkg/errors"
)
@ -94,7 +95,6 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error {
org_name,
org_logo_url,
kolide_server_url,
osquery_enroll_secret,
smtp_configured,
smtp_sender_address,
smtp_server,
@ -121,12 +121,11 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error {
live_query_disabled,
additional_queries
)
VALUES( 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
VALUES( 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
ON DUPLICATE KEY UPDATE
org_name = VALUES(org_name),
org_logo_url = VALUES(org_logo_url),
kolide_server_url = VALUES(kolide_server_url),
osquery_enroll_secret = VALUES(osquery_enroll_secret),
smtp_configured = VALUES(smtp_configured),
smtp_sender_address = VALUES(smtp_sender_address),
smtp_server = VALUES(smtp_server),
@ -158,7 +157,6 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error {
info.OrgName,
info.OrgLogoURL,
info.KolideServerURL,
info.EnrollSecret,
info.SMTPConfigured,
info.SMTPSenderAddress,
info.SMTPServer,
@ -188,3 +186,45 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error {
return err
}
func (d *Datastore) VerifyEnrollSecret(secret string) (string, error) {
var s kolide.EnrollSecret
err := d.db.Get(&s, "SELECT name, active FROM enroll_secrets WHERE secret = ?", secret)
if err != nil {
return "", errors.New("no matching secret found")
}
if !s.Active {
return "", errors.New("secret is inactive")
}
return s.Name, nil
}
func (d *Datastore) ApplyEnrollSecretSpec(spec *kolide.EnrollSecretSpec) error {
err := d.withRetryTxx(func(tx *sqlx.Tx) error {
for _, secret := range spec.Secrets {
sql := `
INSERT INTO enroll_secrets (name, secret, active)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
secret = VALUES(secret),
active = VALUES(active)
`
if _, err := tx.Exec(sql, secret.Name, secret.Secret, secret.Active); err != nil {
return errors.Wrap(err, "upsert secret")
}
}
return nil
})
return err
}
func (d *Datastore) GetEnrollSecretSpec() (*kolide.EnrollSecretSpec, error) {
var spec kolide.EnrollSecretSpec
sql := `SELECT * FROM enroll_secrets`
if err := d.db.Select(&spec.Secrets, sql); err != nil {
return nil, errors.Wrap(err, "get secrets")
}
return &spec, nil
}

View File

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

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 (
"context"
"encoding/json"
"time"
)
// AppConfigStore contains method for saving and retrieving
@ -11,6 +12,16 @@ type AppConfigStore interface {
NewAppConfig(info *AppConfig) (*AppConfig, error)
AppConfig() (*AppConfig, error)
SaveAppConfig(info *AppConfig) error
// VerifyEnrollSecret checks that the provided secret matches an active
// enroll secret. If it is successfully matched, the name of the secret is
// returned. Otherwise an error is returned.
VerifyEnrollSecret(secret string) (string, error)
// ApplyEnrollSecretSpec adds and updates the enroll secrets specified in
// the spec.
ApplyEnrollSecretSpec(spec *EnrollSecretSpec) error
// GetEnrollSecretSpec gets the spec for the current enroll secrets.
GetEnrollSecretSpec() (*EnrollSecretSpec, error)
}
// AppConfigService provides methods for configuring
@ -21,6 +32,12 @@ type AppConfigService interface {
ModifyAppConfig(ctx context.Context, p AppConfigPayload) (info *AppConfig, err error)
SendTestEmail(ctx context.Context, config *AppConfig) error
// ApplyEnrollSecretSpec adds and updates the enroll secrets specified in
// the spec.
ApplyEnrollSecretSpec(ctx context.Context, spec *EnrollSecretSpec) error
// GetEnrollSecretSpec gets the spec for the current enroll secrets.
GetEnrollSecretSpec(ctx context.Context) (*EnrollSecretSpec, error)
// Certificate returns the PEM encoded certificate chain for osqueryd TLS termination.
// For cases where the connection is self-signed, the server will attempt to
// connect using the InsecureSkipVerify option in tls.Config.
@ -84,11 +101,6 @@ type AppConfig struct {
OrgLogoURL string `db:"org_logo_url"`
KolideServerURL string `db:"kolide_server_url"`
// EnrollSecret is the config value that must be given by osqueryd hosts
// on enrollment.
// See https://osquery.readthedocs.io/en/stable/deployment/remote/#remote-authentication
EnrollSecret string `db:"osquery_enroll_secret"`
// SMTPConfigured is a flag that indicates if smtp has been successfully
// tested with the settings provided by an admin user.
SMTPConfigured bool `db:"smtp_configured"`
@ -238,7 +250,6 @@ type OrgInfo struct {
// ServerSettings contains general settings about the kolide App.
type ServerSettings struct {
KolideServerURL *string `json:"kolide_server_url,omitempty"`
EnrollSecret *string `json:"osquery_enroll_secret,omitempty"`
LiveQueryDisabled *bool `json:"live_query_disabled,omitempty"`
}
@ -272,3 +283,23 @@ type ListOptions struct {
// Direction of ordering
OrderDirection OrderDirection
}
// EnrollSecret contains information about an enroll secret, name, and active
// status. Enroll secrets are used for osquery authentication.
type EnrollSecret struct {
// Name is the name assigned to the secret
Name string `json:"name" db:"name"`
// Secret is the actual secret key.
Secret string `json:"secret" db:"secret"`
// Active determines whether the secret is currently allowed to be used for
// authentication.
Active bool `json:"active" db:"active"`
// CreatedAt is the time this enroll secret was first added.
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// EnrollSecretSpec is the fleetctl spec type for enroll secrets.
type EnrollSecretSpec struct {
// Secrets is the list of enroll secrets.
Secrets []EnrollSecret `json:"secrets"`
}

View File

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

View File

@ -12,6 +12,12 @@ type AppConfigFunc func() (*kolide.AppConfig, error)
type SaveAppConfigFunc func(info *kolide.AppConfig) error
type VerifyEnrollSecretFunc func(secret string) (string, error)
type ApplyEnrollSecretSpecFunc func(spec *kolide.EnrollSecretSpec) error
type GetEnrollSecretSpecFunc func() (*kolide.EnrollSecretSpec, error)
type AppConfigStore struct {
NewAppConfigFunc NewAppConfigFunc
NewAppConfigFuncInvoked bool
@ -21,6 +27,15 @@ type AppConfigStore struct {
SaveAppConfigFunc SaveAppConfigFunc
SaveAppConfigFuncInvoked bool
VerifyEnrollSecretFunc VerifyEnrollSecretFunc
VerifyEnrollSecretFuncInvoked bool
ApplyEnrollSecretSpecFunc ApplyEnrollSecretSpecFunc
ApplyEnrollSecretSpecFuncInvoked bool
GetEnrollSecretSpecFunc GetEnrollSecretSpecFunc
GetEnrollSecretSpecFuncInvoked bool
}
func (s *AppConfigStore) NewAppConfig(info *kolide.AppConfig) (*kolide.AppConfig, error) {
@ -37,3 +52,18 @@ func (s *AppConfigStore) SaveAppConfig(info *kolide.AppConfig) error {
s.SaveAppConfigFuncInvoked = true
return s.SaveAppConfigFunc(info)
}
func (s *AppConfigStore) VerifyEnrollSecret(secret string) (string, error) {
s.VerifyEnrollSecretFuncInvoked = true
return s.VerifyEnrollSecretFunc(secret)
}
func (s *AppConfigStore) ApplyEnrollSecretSpec(spec *kolide.EnrollSecretSpec) error {
s.ApplyEnrollSecretSpecFuncInvoked = true
return s.ApplyEnrollSecretSpecFunc(spec)
}
func (s *AppConfigStore) GetEnrollSecretSpec() (*kolide.EnrollSecretSpec, error) {
s.GetEnrollSecretSpecFuncInvoked = true
return s.GetEnrollSecretSpecFunc()
}

View File

@ -20,7 +20,7 @@ type HostFunc func(id uint) (*kolide.Host, error)
type ListHostsFunc func(opt kolide.ListOptions) ([]*kolide.Host, error)
type EnrollHostFunc func(osqueryHostId string, nodeKeySize int) (*kolide.Host, error)
type EnrollHostFunc func(osqueryHostId, nodeKey, secretName string) (*kolide.Host, error)
type AuthenticateHostFunc func(nodeKey string) (*kolide.Host, error)
@ -102,9 +102,9 @@ func (s *HostStore) ListHosts(opt kolide.ListOptions) ([]*kolide.Host, error) {
return s.ListHostsFunc(opt)
}
func (s *HostStore) EnrollHost(osqueryHostId string, nodeKeySize int) (*kolide.Host, error) {
func (s *HostStore) EnrollHost(osqueryHostId, nodeKey, secretName string) (*kolide.Host, error) {
s.EnrollHostFuncInvoked = true
return s.EnrollHostFunc(osqueryHostId, nodeKeySize)
return s.EnrollHostFunc(osqueryHostId, nodeKey, secretName)
}
func (s *HostStore) AuthenticateHost(nodeKey string) (*kolide.Host, error) {

View File

@ -69,3 +69,61 @@ func (c *Client) GetServerSettings() (*kolide.ServerSettings, error) {
}
return appConfig.ServerSettings, nil
}
// GetEnrollSecretSpec fetches the enroll secrets stored on the server
func (c *Client) GetEnrollSecretSpec() (*kolide.EnrollSecretSpec, error) {
response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/spec/enroll_secret", nil)
if err != nil {
return nil, errors.Wrap(err, "GET /api/v1/kolide/spec/enroll_secret")
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, errors.Errorf(
"get enroll_secrets received status %d %s",
response.StatusCode,
extractServerErrorText(response.Body),
)
}
var responseBody getEnrollSecretSpecResponse
err = json.NewDecoder(response.Body).Decode(&responseBody)
if err != nil {
return nil, errors.Wrap(err, "decode get enroll secret spec response")
}
if responseBody.Err != nil {
return nil, errors.Errorf("get enroll secret spec: %s", responseBody.Err)
}
return responseBody.Spec, nil
}
// ApplyEnrollSecretSpec applies the enroll secrets.
func (c *Client) ApplyEnrollSecretSpec(spec *kolide.EnrollSecretSpec) error {
req := applyEnrollSecretSpecRequest{Spec: spec}
response, err := c.AuthenticatedDo("POST", "/api/v1/kolide/spec/enroll_secret", req)
if err != nil {
return errors.Wrap(err, "POST /api/v1/kolide/spec/enroll_secret")
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return errors.Errorf(
"apply enroll secret received status %d %s",
response.StatusCode,
extractServerErrorText(response.Body),
)
}
var responseBody applyEnrollSecretSpecResponse
err = json.NewDecoder(response.Body).Decode(&responseBody)
if err != nil {
return errors.Wrap(err, "decode apply enroll secret response")
}
if responseBody.Err != nil {
return errors.Errorf("apply enroll secret: %s", responseBody.Err)
}
return nil
}

View File

@ -65,7 +65,6 @@ func makeGetAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint {
},
ServerSettings: &kolide.ServerSettings{
KolideServerURL: &config.KolideServerURL,
EnrollSecret: &config.EnrollSecret,
LiveQueryDisabled: &config.LiveQueryDisabled,
},
SMTPSettings: smtpSettings,
@ -93,7 +92,6 @@ func makeModifyAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint {
},
ServerSettings: &kolide.ServerSettings{
KolideServerURL: &config.KolideServerURL,
EnrollSecret: &config.EnrollSecret,
LiveQueryDisabled: &config.LiveQueryDisabled,
},
SMTPSettings: smtpSettingsFromAppConfig(config),
@ -137,3 +135,49 @@ func smtpSettingsFromAppConfig(config *kolide.AppConfig) *kolide.SMTPSettingsPay
SMTPEnableStartTLS: &config.SMTPEnableStartTLS,
}
}
////////////////////////////////////////////////////////////////////////////////
// Apply Enroll Secret Spec
////////////////////////////////////////////////////////////////////////////////
type applyEnrollSecretSpecRequest struct {
Spec *kolide.EnrollSecretSpec `json:"spec"`
}
type applyEnrollSecretSpecResponse struct {
Err error `json:"error,omitempty"`
}
func (r applyEnrollSecretSpecResponse) error() error { return r.Err }
func makeApplyEnrollSecretSpecEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(applyEnrollSecretSpecRequest)
err := svc.ApplyEnrollSecretSpec(ctx, req.Spec)
if err != nil {
return applyEnrollSecretSpecResponse{Err: err}, nil
}
return applyEnrollSecretSpecResponse{}, nil
}
}
////////////////////////////////////////////////////////////////////////////////
// Get Pack Specs
////////////////////////////////////////////////////////////////////////////////
type getEnrollSecretSpecResponse struct {
Spec *kolide.EnrollSecretSpec `json:"specs"`
Err error `json:"error,omitempty"`
}
func (r getEnrollSecretSpecResponse) error() error { return r.Err }
func makeGetEnrollSecretSpecEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
specs, err := svc.GetEnrollSecretSpec(ctx)
if err != nil {
return getEnrollSecretSpecResponse{Err: err}, nil
}
return getEnrollSecretSpecResponse{Spec: specs}, nil
}
}

View File

@ -3,12 +3,16 @@ package service
import (
"context"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/kolide/fleet/server/config"
hostctx "github.com/kolide/fleet/server/contexts/host"
"github.com/kolide/fleet/server/contexts/viewer"
"github.com/kolide/fleet/server/datastore/inmem"
"github.com/kolide/fleet/server/kolide"
"github.com/kolide/fleet/server/mock"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -197,25 +201,36 @@ func TestGetNodeKey(t *testing.T) {
}
func TestAuthenticatedHost(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
_, err = ds.NewAppConfig(&kolide.AppConfig{EnrollSecret: "foobarbaz"})
require.Nil(t, err)
ds := new(mock.Store)
svc, err := newTestService(ds, nil)
require.Nil(t, err)
expectedHost := kolide.Host{HostName: "foo!"}
goodNodeKey := "foo bar baz bing bang boom"
ds.AuthenticateHostFunc = func(secret string) (*kolide.Host, error) {
switch secret {
case goodNodeKey:
return &expectedHost, nil
default:
return nil, errors.New("no host found")
}
}
ds.MarkHostSeenFunc = func(host *kolide.Host, t time.Time) error {
return nil
}
endpoint := authenticatedHost(
svc,
func(ctx context.Context, request interface{}) (interface{}, error) {
host, ok := hostctx.FromContext(ctx)
assert.True(t, ok)
assert.Equal(t, expectedHost, host)
return nil, nil
},
)
ctx := context.Background()
goodNodeKey, err := svc.EnrollAgent(ctx, "foobarbaz", "host123", nil)
assert.Nil(t, err)
require.NotEmpty(t, goodNodeKey)
var authenticatedHostTests = []struct {
nodeKey string
shouldErr bool

View File

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

View File

@ -36,6 +36,8 @@ type KolideEndpoints struct {
DeleteSession endpoint.Endpoint
GetAppConfig endpoint.Endpoint
ModifyAppConfig endpoint.Endpoint
ApplyEnrollSecretSpec endpoint.Endpoint
GetEnrollSecretSpec endpoint.Endpoint
CreateInvite endpoint.Endpoint
ListInvites endpoint.Endpoint
DeleteInvite endpoint.Endpoint
@ -138,6 +140,8 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string) Kol
DeleteSession: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteSessionEndpoint(svc))),
GetAppConfig: authenticatedUser(jwtKey, svc, canPerformActions(makeGetAppConfigEndpoint(svc))),
ModifyAppConfig: authenticatedUser(jwtKey, svc, mustBeAdmin(makeModifyAppConfigEndpoint(svc))),
ApplyEnrollSecretSpec: authenticatedUser(jwtKey, svc, mustBeAdmin(makeApplyEnrollSecretSpecEndpoint(svc))),
GetEnrollSecretSpec: authenticatedUser(jwtKey, svc, canPerformActions(makeGetEnrollSecretSpecEndpoint(svc))),
CreateInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeCreateInviteEndpoint(svc))),
ListInvites: authenticatedUser(jwtKey, svc, mustBeAdmin(makeListInvitesEndpoint(svc))),
DeleteInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteInviteEndpoint(svc))),
@ -225,6 +229,8 @@ type kolideHandlers struct {
DeleteSession http.Handler
GetAppConfig http.Handler
ModifyAppConfig http.Handler
ApplyEnrollSecretSpec http.Handler
GetEnrollSecretSpec http.Handler
CreateInvite http.Handler
ListInvites http.Handler
DeleteInvite http.Handler
@ -315,6 +321,8 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli
DeleteSession: newServer(e.DeleteSession, decodeDeleteSessionRequest),
GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest),
ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest),
ApplyEnrollSecretSpec: newServer(e.ApplyEnrollSecretSpec, decodeApplyEnrollSecretSpecRequest),
GetEnrollSecretSpec: newServer(e.GetEnrollSecretSpec, decodeNoParamsRequest),
CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest),
ListInvites: newServer(e.ListInvites, decodeListInvitesRequest),
DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest),
@ -446,6 +454,8 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) {
r.Handle("/api/v1/kolide/config/certificate", h.GetCertificate).Methods("GET").Name("get_certificate")
r.Handle("/api/v1/kolide/config", h.GetAppConfig).Methods("GET").Name("get_app_config")
r.Handle("/api/v1/kolide/config", h.ModifyAppConfig).Methods("PATCH").Name("modify_app_config")
r.Handle("/api/v1/kolide/spec/enroll_secret", h.ApplyEnrollSecretSpec).Methods("POST").Name("apply_enroll_secret_spec")
r.Handle("/api/v1/kolide/spec/enroll_secret", h.GetEnrollSecretSpec).Methods("GET").Name("get_enroll_secret_spec")
r.Handle("/api/v1/kolide/invites", h.CreateInvite).Methods("POST").Name("create_invite")
r.Handle("/api/v1/kolide/invites", h.ListInvites).Methods("GET").Name("list_invites")
r.Handle("/api/v1/kolide/invites/{id}", h.DeleteInvite).Methods("DELETE").Name("delete_invite")

View File

@ -36,18 +36,31 @@ func (svc service) NewAppConfig(ctx context.Context, p kolide.AppConfigPayload)
return nil, err
}
fromPayload := appConfigFromAppConfigPayload(p, *config)
if fromPayload.EnrollSecret == "" {
// generate a random string if the user hasn't set one in the form.
rand, err := kolide.RandomText(24)
if err != nil {
return nil, errors.Wrap(err, "generate enroll secret string")
}
fromPayload.EnrollSecret = rand
}
newConfig, err := svc.ds.NewAppConfig(fromPayload)
if err != nil {
return nil, err
}
// Set up a default enroll secret
secret, err := kolide.RandomText(24)
if err != nil {
return nil, errors.Wrap(err, "generate enroll secret string")
}
spec := &kolide.EnrollSecretSpec{
Secrets: []kolide.EnrollSecret{
kolide.EnrollSecret{
Name: "default",
Secret: secret,
Active: true,
},
},
}
err = svc.ds.ApplyEnrollSecretSpec(spec)
if err != nil {
return nil, errors.Wrap(err, "save enroll secret")
}
return newConfig, nil
}
@ -117,9 +130,6 @@ func appConfigFromAppConfigPayload(p kolide.AppConfigPayload, config kolide.AppC
if p.ServerSettings != nil && p.ServerSettings.KolideServerURL != nil {
config.KolideServerURL = cleanupURL(*p.ServerSettings.KolideServerURL)
}
if p.ServerSettings != nil && p.ServerSettings.EnrollSecret != nil {
config.EnrollSecret = *p.ServerSettings.EnrollSecret
}
if p.ServerSettings != nil && p.ServerSettings.LiveQueryDisabled != nil {
config.LiveQueryDisabled = *p.ServerSettings.LiveQueryDisabled
}
@ -229,3 +239,11 @@ func appConfigFromAppConfigPayload(p kolide.AppConfigPayload, config kolide.AppC
}
return &config
}
func (svc service) ApplyEnrollSecretSpec(ctx context.Context, spec *kolide.EnrollSecretSpec) error {
return svc.ds.ApplyEnrollSecretSpec(spec)
}
func (svc service) GetEnrollSecretSpec(ctx context.Context) (*kolide.EnrollSecretSpec, error) {
return svc.ds.GetEnrollSecretSpec()
}

View File

@ -4,9 +4,8 @@ import (
"context"
"testing"
"github.com/kolide/fleet/server/config"
"github.com/kolide/fleet/server/datastore/inmem"
"github.com/kolide/fleet/server/kolide"
"github.com/kolide/fleet/server/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -32,12 +31,14 @@ func TestCleanupURL(t *testing.T) {
}
func TestCreateAppConfig(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
require.Nil(t, ds.MigrateData())
ds := new(mock.Store)
svc, err := newTestService(ds, nil)
require.Nil(t, err)
ds.AppConfigFunc = func() (*kolide.AppConfig, error) {
return &kolide.AppConfig{}, nil
}
var appConfigTests = []struct {
configPayload kolide.AppConfigPayload
}{
@ -56,14 +57,30 @@ func TestCreateAppConfig(t *testing.T) {
}
for _, tt := range appConfigTests {
result, err := svc.NewAppConfig(context.Background(), tt.configPayload)
var result *kolide.AppConfig
ds.NewAppConfigFunc = func(config *kolide.AppConfig) (*kolide.AppConfig, error) {
result = config
return config, nil
}
var gotSecretSpec *kolide.EnrollSecretSpec
ds.ApplyEnrollSecretSpecFunc = func(spec *kolide.EnrollSecretSpec) error {
gotSecretSpec = spec
return nil
}
_, err := svc.NewAppConfig(context.Background(), tt.configPayload)
require.Nil(t, err)
payload := tt.configPayload
assert.NotEmpty(t, result.ID)
assert.Equal(t, *payload.OrgInfo.OrgLogoURL, result.OrgLogoURL)
assert.Equal(t, *payload.OrgInfo.OrgName, result.OrgName)
assert.Equal(t, "https://acme.co:8080", result.KolideServerURL)
assert.Equal(t, *payload.ServerSettings.LiveQueryDisabled, result.LiveQueryDisabled)
// Ensure enroll secret was set
require.NotNil(t, gotSecretSpec)
require.Len(t, gotSecretSpec.Secrets, 1)
assert.Len(t, gotSecretSpec.Secrets[0].Secret, 32)
}
}

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) {
config, err := svc.ds.AppConfig()
secretName, err := svc.ds.VerifyEnrollSecret(enrollSecret)
if err != nil {
return "", osqueryError{message: "getting enroll secret: " + err.Error(), nodeInvalid: true}
return "", osqueryError{
message: "enroll failed: " + err.Error(),
nodeInvalid: true,
}
}
if enrollSecret != config.EnrollSecret {
return "", osqueryError{message: "invalid enroll secret", nodeInvalid: true}
nodeKey, err := kolide.RandomText(svc.config.Osquery.NodeKeySize)
if err != nil {
return "", osqueryError{
message: "generate node key failed: " + err.Error(),
nodeInvalid: true,
}
}
host, err := svc.ds.EnrollHost(hostIdentifier, svc.config.Osquery.NodeKeySize)
host, err := svc.ds.EnrollHost(hostIdentifier, nodeKey, secretName)
if err != nil {
return "", osqueryError{message: "enrollment failed: " + err.Error(), nodeInvalid: true}
return "", osqueryError{message: "save enroll failed: " + err.Error(), nodeInvalid: true}
}
// Save enrollment details if provided

View File

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

View File

@ -15,3 +15,12 @@ func decodeModifyAppConfigRequest(ctx context.Context, r *http.Request) (interfa
}
return appConfigRequest{Payload: payload}, nil
}
func decodeApplyEnrollSecretSpecRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req applyEnrollSecretSpecRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return req, nil
}