mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
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:
parent
619e36755c
commit
c1aa8355cb
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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!
|
||||||
|
```
|
||||||
|
@ -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)
|
||||||
|
14
examples/config-many-files/enroll-secret.yml
Normal file
14
examples/config-many-files/enroll-secret.yml
Normal 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!
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 };
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,9 @@
|
|||||||
|
.enroll-secrets {
|
||||||
|
max-height: 10em;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
1
frontend/components/config/EnrollSecretTable/index.js
Normal file
1
frontend/components/config/EnrollSecretTable/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default from './EnrollSecretTable';
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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`}>
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
10
frontend/interfaces/enroll_secret.js
Normal file
10
frontend/interfaces/enroll_secret.js
Normal 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,
|
||||||
|
}),
|
||||||
|
);
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -56,6 +56,7 @@ describe('ManageHostsPage - component', () => {
|
|||||||
loadingLabels: false,
|
loadingLabels: false,
|
||||||
selectedOsqueryTable: stubbedOsqueryTable,
|
selectedOsqueryTable: stubbedOsqueryTable,
|
||||||
statusLabels: {},
|
statusLabels: {},
|
||||||
|
enrollSecret: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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.';
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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"`
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user